meyton-sync – Anleitung

## Übersicht

meyton-sync synchronisiert Schießergebnisse aus der lokalen Datenbank der Schießanlage kontinuierlich auf einen externen Webserver. Benutzer und Applikationen können die Ergebnisse dort über eine REST-API abrufen.

Source Schießanlage
SSMDB2 (MySQL)
→ HTTPS →
Target Webserver
FastAPI + MariaDB
→
Clients REST-API
Admin-UI

Das System besteht aus zwei unabhängigen Komponenten:

- Source – läuft auf einem Rechner im Netz der Schießanlage, liest Änderungen aus der Datenbank `SSMDB2` und sendet sie gebündelt per HTTPS an den Zielserver.
- Target – läuft auf dem externen Webserver, empfängt die Datenpakete, speichert sie in der Replikationsdatenbank `SSMDB2c` und stellt sie über eine REST-API sowie eine Admin-Oberfläche bereit.

### Datenbankstruktur

Tabelle | Inhalt
--- | ---
`Scheiben` | Treffer-Scheiben (Schütze, Disziplin, Gesamtergebnis, …)
`Serien` | Serien je Scheibe (Stellung, Ringergebnis)
`Treffer` | Einzeltreffer mit X/Y-Koordinaten, Teiler, Qualität
`Users` | API-Benutzer mit Rollen und Passwort-Hashes

## Voraussetzungen

### Source (Schießanlage)

Anforderung | Details
--- | ---
Betriebssystem | openSUSE 15.2+, SLES, Debian oder Ubuntu
Python | Python 3
Python-Pakete | `PyMySQL`, `requests`, `apscheduler`
Datenbank | Lesezugriff auf `SSMDB2` (MySQL/MariaDB)
Netzwerk | Ausgehende HTTPS-Verbindung (Port 443) zum Zielserver

### Target (Webserver)

Anforderung | Details
--- | ---
Betriebssystem | Debian oder Ubuntu (Pflicht)
Python | Python 3
Python-Pakete | `FastAPI`, `Uvicorn`, `python-multipart`, `passlib`, `bcrypt`, `PyJWT`, `PyMySQL`
Webserver | Apache 2 mit `mod_proxy` und `mod_proxy_http`
Datenbank | MariaDB/MySQL lokal (wird automatisch eingerichtet)
Netzwerk | HTTPS-Erreichbarkeit; Uvicorn läuft auf `127.0.0.1:8000`

Hinweis Die Installations-Skripte für die Target-Komponente funktionieren ausschließlich unter Debian/Ubuntu. Für andere Distributionen ist eine manuelle Installation erforderlich.

## Installation

### Source installieren

1. Installationsskript auf dem Quell-Rechner ausführen:

       bash install-source.sh

2. Das Skript fragt interaktiv nach den Datenbankzugangsdaten, der Ziel-URL sowie den Synchronisierungstoken.
3. Anschließend wird der Systemd-Dienst `meyton-sync-source` angelegt und gestartet.
4. Konfigurationsdatei prüfen:
   `/opt/meyton-sync/source/source.conf`

#### source.conf – Konfigurationsparameter

    [database]
    host     = 192.168.10.200   # IP der SSMDB2-Datenbank
    port     = 3306
    user     = meyton
    password = <Passwort>
    database = SSMDB2

    [sync]
    target_url      = https://www.mydomain.de/meyton/sync
    own_token       = <32-Byte-Hex>     # Token dieser Source
    target_token    = <32-Byte-Hex>     # Erwarteter Token des Targets
    interval        = 60                 # Sekunden zwischen Sync-Läufen (1–600)
    stands          = 15                 # Anzahl Schießstände
    batch_size      = 100                # Scheiben pro Batch
    request_timeout = 30

    [logging]
    level = INFO
    file  = source.log

### Target installieren

1. Installationsskript auf dem Webserver ausführen (Root-Rechte erforderlich):

       sudo bash install-target.sh

2. Das Skript richtet Datenbank, Benutzer und Apache-Proxy automatisch ein.
3. Systemd-Dienst `meyton-sync-target` wird gestartet; die API ist anschließend unter `/meyton/` erreichbar.
4. Konfigurationsdatei prüfen:
   `/opt/meyton-sync/target/target.conf`

#### target.conf – Konfigurationsparameter

    [database]
    host     = localhost
    port     = 3306
    user     = meyton
    password = <Passwort>
    database = SSMDB2c

    [sync]
    own_token    = <32-Byte-Hex>     # Token dieses Targets
    source_token = <32-Byte-Hex>     # Erwarteter Token der Source

    [api]
    secret_key            = <64-Byte-Hex>   # JWT-Signing-Key
    token_expire_minutes  = 60

    [logging]
    level = INFO
    file  = target.log

### Apache-Reverse-Proxy

Das Installationsskript fügt in die aktive Apache-Konfiguration ein:

    ProxyPass        /meyton/  http://127.0.0.1:8000/
    ProxyPassReverse /meyton/  http://127.0.0.1:8000/

### Admin-Passwort zurücksetzen

Falls das Admin-Passwort verloren geht, kann es mit folgendem Hilfsprogramm zurückgesetzt werden:

    cd /opt/meyton-sync/target
    python3 set_admin_password.py

Sicherheit Das Standard-Passwort des Admin-Kontos lautet `erhkm-673BX#a`. Es muss direkt nach der Installation in der Admin-Oberfläche geändert werden.

### Dienste verwalten

    # Status prüfen
    systemctl status meyton-sync-source
    systemctl status meyton-sync-target

    # Starten / Stoppen / Neustarten
    systemctl start  meyton-sync-target
    systemctl stop   meyton-sync-target
    systemctl restart meyton-sync-target

    # Log-Ausgabe
    journalctl -u meyton-sync-target -f

## Funktionsweise Sync

### Ablauf auf Source-Seite

Der Source-Prozess läuft als systemd-Dienst und führt in regelmäßigen Abständen (Standard: 60 s) einen Sync-Lauf durch:

1. Zustandsprüfung: Beim ersten Start (keine `source.state`-Datei) oder wenn der Target 0 Scheiben meldet, wird ein vollständiger Sync durchgeführt.
2. Inkrementeller Sync: Danach werden nur Datensätze übertragen, deren `Zeitstempel` nach dem letzten erfolgreichen Sync liegt.
3. Batching: Scheiben werden in Paketen (Standard: 100) zusammengefasst. Zu jeder Scheibe werden die zugehörigen Serien und Treffer mitgesendet.
4. Komprimierung: Jedes Paket wird per gzip komprimiert und per HTTP-POST an den Target gesendet.
5. Zustandsspeicherung: Nach erfolgreichem Versand wird der Zeitstempel in `source.state` aktualisiert.

### Ablauf auf Target-Seite

1. Eingehende Anfragen werden anhand des Bearer-Tokens authentifiziert.
2. Die empfangenen Scheiben werden per UPSERT in die Datenbank geschrieben (`INSERT … ON DUPLICATE KEY UPDATE`).
3. Zugehörige Serien und Treffer werden gelöscht und neu eingefügt, um Inkonsistenzen zu vermeiden.
4. Anzahl und Typ (vollständig / inkrementell) werden protokolliert.

### Token-Authentifizierung

Source und Target tauschen bei jedem Sync-Aufruf zwei Pre-Shared Tokens aus:

- own_token (Source) / source_token (Target): Identifiziert die Source.
- target_token (Source) / own_token (Target): Bestätigt den echten Target-Server.

Beide Token müssen auf beiden Seiten übereinstimmen, sonst wird der Sync abgelehnt.

### Kompatibilität älterer Datenbanken

Hinweis Ältere SSMDB2-Versionen (vor 5.4) enthalten die Spalten `StandNrText` und `Serie` noch nicht. meyton-sync erkennt dies automatisch und passt die Abfragen entsprechend an.

## REST-API

Alle API-Endpunkte sind unter `https://<server>/meyton/api/` erreichbar. Eine interaktive Swagger-Dokumentation steht unter `/meyton/docs` zur Verfügung.

### Authentifizierung

Die API verwendet JWT-Bearer-Token. Token werden über den Login-Endpunkt bezogen und sind standardmäßig 60 Minuten gültig.

    # Login (gibt JWT-Token zurück)
    POST /meyton/api/token
    Content-Type: application/x-www-form-urlencoded

    username=<login>&password=<passwort>

Den Token anschließend im `Authorization`-Header mitsenden:

    Authorization: Bearer <token>

### Scheiben-Endpunkte

Methode | Pfad | Beschreibung | Mindestrolle
--- | --- | --- | ---
GET | `/api/scheiben/today` | Alle Scheiben des heutigen Tages | user
GET | `/api/scheiben/last-per-stand` | Letzte Scheibe je Stand | user
GET | `/api/scheiben/sportpass/{id}?limit=20` | Scheiben eines Schützen nach Sportpass-ID | restricted / singe *
GET | `/api/scheiben/shooter?nachname=X&vorname=Y&limit=20` | Scheiben nach Name des Schützen | restricted / singe *
GET | `/api/scheibe/{scheiben_id}` | Vollständige Scheibe mit Serien und Einzeltreffern | restricted / singe *

* `singe`-Benutzer erhalten nur Ergebnisse, die zu ihrem eigenen Profil gehören – alle anderen Anfragen werden mit `403` abgelehnt. Weitere Details siehe Singe-Rolle.

### Benutzerverwaltung (nur Admin)

Methode | Pfad | Beschreibung
--- | --- | ---
GET | `/api/users` | Alle Benutzer auflisten (inkl. Nachname, Vorname, SportpassID)
POST | `/api/users` | Neuen Benutzer anlegen (Rollen: `user`, `restricted` oder `singe`; bei `singe` sind Nachname und Vorname Pflicht)
DELETE | `/api/users/{login}` | Benutzer löschen (Admin-Konto kann nicht gelöscht werden)
PUT | `/api/users/{login}/password` | Passwort eines Benutzers ändern
PUT | `/api/users/{login}/role` | Rolle eines Benutzers ändern; bei Wechsel auf `singe` müssen Nachname und Vorname mitübergeben werden
PUT | `/api/admin/password` | Admin-Passwort ändern

### Datenbankoperationen (nur Admin)

Methode | Pfad | Beschreibung
--- | --- | ---
GET | `/api/db/disziplinen` | Alle Disziplinen auflisten
GET | `/api/db/starterlisten` | Alle Starterlisten auflisten
DELETE | `/api/db/clearErgebnisse` | Alle Ergebnisse löschen
DELETE | `/api/db/clearErgebnisseDisziplin?disziplin=X` | Ergebnisse einer Disziplin löschen
DELETE | `/api/db/clearErgebnisseStarterliste?starterliste=X` | Ergebnisse einer Starterliste löschen

### Sync-Endpunkte (intern)

Diese Endpunkte werden ausschließlich von der Source-Komponente verwendet und sind nicht in der Swagger-Dokumentation sichtbar.

Methode | Pfad | Beschreibung
--- | --- | ---
GET | `/sync/status` | Liefert Token und aktuelle Scheiben-Anzahl zurück
POST | `/sync` | Empfängt komprimiertes Batch-Paket mit Scheiben-Daten

### Passwort-Anforderungen

- Mindestens 10 Zeichen
- Muss sowohl Buchstaben als auch Ziffern enthalten

## Rollen & Berechtigungen

Jeder Benutzer hat genau eine der vier Rollen. Die Rolle wird beim Login in den JWT-Token eingebettet und bei jeder Anfrage geprüft.

Rolle | Beschreibung | Zugang
--- | --- | ---
`admin` | Systemadministrator | Vollzugriff auf alle Endpunkte inkl. Benutzerverwaltung und Datenbank-Operationen
`user` | Normaler Benutzer | Lesezugriff auf alle Scheiben-Endpunkte (`today`, `last-per-stand`, Suche nach Name/SportpassID, Einzelscheibe)
`restricted` | Eingeschränkter Benutzer | Nur Schützen-bezogene Suchen (`sportpass`, `shooter`, Einzelscheibe) – kein Zugriff auf Tages- oder Standübersichten; keine Filterung nach Eigentümer
`singe` | Einzelschütze (persönlich) | Wie `restricted`, aber alle Suchergebnisse werden auf das eigene Profil (Nachname, Vorname, SportpassID) gefiltert. Fremde Ergebnisse werden mit `403` abgelehnt.

### Berechtigungsmatrix

Endpunkt | admin | user | restricted | singe
--- | --- | --- | --- | ---
`scheiben/today` | ✓ | ✓ | ✗ | ✗
`scheiben/last-per-stand` | ✓ | ✓ | ✗ | ✗
`scheiben/sportpass/{id}` | ✓ | ✓ | ✓ | nur eigene
`scheiben/shooter` | ✓ | ✓ | ✓ | nur eigene
`scheibe/{id}` | ✓ | ✓ | ✓ | nur eigene
Benutzerverwaltung | ✓ | ✗ | ✗ | ✗
Datenbankoperationen | ✓ | ✗ | ✗ | ✗
Statistik (`stats`) | ✓ | ✗ | ✗ | ✗

## Singe-Rolle

Die Rolle `singe` ist für Einzelschützen gedacht, die ausschließlich ihre eigenen Ergebnisse abrufen dürfen. Sie hat dieselben Zugriffsrechte wie `restricted`, mit dem Unterschied, dass alle zurückgegebenen Ergebnisse serverseitig auf das Profil des eingeloggten Benutzers gefiltert werden.

### Profilfelder

Jeder `singe`-Benutzer hat in der Datenbank drei zusätzliche Felder:

Feld | Typ | Beschreibung
--- | --- | ---
`Nachname` | Text (max. 50 Zeichen) | Nachname des Schützen, wie er in den Scheiben-Daten steht (Pflicht)
`Vorname` | Text (max. 50 Zeichen) | Vorname des Schützen (Pflicht)
`SportpassID` | Ganzzahl (bigint) | Sportpass-Nummer des Schützen (optional, 0 = nicht gesetzt)

Hinweis Diese Felder existieren in der `Users`-Tabelle für alle Benutzerrollen; bei `admin`, `user` und `restricted` bleiben sie leer und haben keine Auswirkung auf die API-Filterung.

### Filterlogik

Die Profilfelder werden beim Login in den JWT-Token eingebettet und bei jeder Anfrage serverseitig ausgewertet:

#### `GET /api/scheiben/sportpass/{sportpass_id}`

Der angefragte Wert `sportpass_id` muss exakt mit der im Profil hinterlegten `SportpassID` übereinstimmen. Jede andere ID wird mit `403 Keine Berechtigung für diese SportpassID` abgewiesen.

#### `GET /api/scheiben/shooter?nachname=…&vorname=…`

Nachname und Vorname aus der Anfrage müssen (Groß-/Kleinschreibung wird ignoriert) mit den Profilfeldern übereinstimmen. Abweichende Namen werden mit `403 Keine Berechtigung für diesen Schützen` abgewiesen.

#### `GET /api/scheibe/{scheiben_id}`

Der Server prüft, ob die Scheibe dem Benutzer gehört. Die Prüfung erfolgt in dieser Reihenfolge:

1. Wenn `SportpassID > 0`: Scheibe muss dieselbe SportpassID haben.
2. Sonst: Nachname und Vorname der Scheibe müssen mit dem Profil übereinstimmen.

Gehört die Scheibe nicht zum Benutzer, wird `403 Keine Berechtigung für diese Scheibe` zurückgegeben.

### Benutzer anlegen

Beim Anlegen über `POST /api/users` müssen für die Rolle `singe` mindestens `nachname` und `vorname` angegeben werden. Die `sportpass_id` ist optional (Standard: 0).

    POST /meyton/api/users
    Authorization: Bearer <admin-token>
    Content-Type: application/json

    {
      "login":        "mueller.hans",
      "password":     "Passwort123!",
      "role":         "singe",
      "nachname":     "Mueller",
      "vorname":      "Hans",
      "sportpass_id": 1234567
    }

### Rolle nachträglich ändern

Beim Wechsel auf `singe` über `PUT /api/users/{login}/role` müssen Nachname und Vorname ebenfalls mitübergeben werden:

    PUT /meyton/api/users/mueller.hans/role
    Authorization: Bearer <admin-token>
    Content-Type: application/json

    {
      "role":         "singe",
      "nachname":     "Mueller",
      "vorname":      "Hans",
      "sportpass_id": 1234567
    }

Achtung Beim Rollenwechsel werden die Felder `Nachname`, `Vorname` und `SportpassID` immer überschrieben – auch wenn die neue Rolle nicht `singe` ist. Bei einem Wechsel zurück auf `user` oder `restricted` können die Felder leer gelassen werden; sie haben dann keinen Einfluss auf das Verhalten der API.

## Statistik-API

Der Endpunkt `GET /api/stats` liefert eine Übersicht über den aktuellen Datenbankinhalt sowie den Synchronisierungsstatus. Er ist ausschließlich für die Rolle `admin` zugänglich.

    GET /meyton/api/stats
    Authorization: Bearer <admin-token>

### Antwortfelder

Feld | Typ | Beschreibung
--- | --- | ---
`scheiben` | Ganzzahl | Gesamtanzahl der gespeicherten Scheiben
`serien` | Ganzzahl | Gesamtanzahl der Serien-Einträge
`treffer` | Ganzzahl | Gesamtanzahl der Einzeltreffer
`scheiben_pro_disziplin` | Liste | Anzahl Scheiben je Disziplin (`disziplin` + `count`), absteigend sortiert
`letzter_verbindungsaufbau` | ISO-8601-Zeitstempel / null | Zeitpunkt des letzten `GET /sync/status`-Aufrufs durch die Source-Komponente
`letzte_synchronisierung` | ISO-8601-Zeitstempel / null | Zeitpunkt des letzten erfolgreich empfangenen Sync-Pakets
`letzte_scheibe` | ISO-8601-Zeitstempel / null | Zeitstempel der jüngsten Scheibe in der Datenbank

### Beispielantwort

    {
      "scheiben": 4821,
      "serien":   28926,
      "treffer":  144630,
      "scheiben_pro_disziplin": [
        { "disziplin": "LG", "count": 2410 },
        { "disziplin": "LP", "count": 1837 },
        { "disziplin": "KK", "count":  574 }
      ],
      "letzter_verbindungsaufbau": "2026-05-10T08:15:02+00:00",
      "letzte_synchronisierung":   "2026-05-10T08:15:05+00:00",
      "letzte_scheibe":            "2026-05-10T08:14:51"
    }

Sync-Überwachung Die Felder `letzter_verbindungsaufbau` und `letzte_synchronisierung` erlauben eine einfache Überwachung des Sync-Prozesses. Liegt `letzter_verbindungsaufbau` mehr als zwei Sync-Intervalle (Standard: 2 × 60 s) in der Vergangenheit, ist die Verbindung zur Schießanlage unterbrochen. `letzte_synchronisierung` kann älter sein, wenn in der Zwischenzeit keine neuen Scheiben erfasst wurden.

## Admin-Oberfläche

Die Admin-Oberfläche ist unter `https://<server>/meyton/admin` erreichbar und steht ausschließlich Benutzern mit der Rolle `admin` zur Verfügung.

### Login

Nach dem Aufrufen der Seite erscheint ein Login-Formular. Nach erfolgreicher Anmeldung wird ein JWT-Token im Browser gespeichert und alle weiteren Aktionen laufen im Hintergrund über die REST-API.

### Tab: Benutzerverwaltung

- Alle vorhandenen Benutzer mit Rolle, Nachname, Vorname und SportpassID anzeigen
- Neuen Benutzer anlegen (Rollen: `user`, `restricted` oder `singe`)
- Bei Rolle `singe`: Nachname und Vorname sind Pflichtfelder; SportpassID ist optional
- Passwort eines bestehenden Benutzers ändern
- Rolle eines Benutzers ändern (außer Admin); beim Wechsel auf `singe` müssen Nachname und Vorname angegeben werden
- Benutzer löschen (außer Admin)

Hinweis Das Admin-Konto selbst kann nicht gelöscht werden und seine Rolle kann nicht geändert werden. Das Admin-Passwort kann über den Endpunkt `PUT /api/admin/password` oder über das Hilfsskript `set_admin_password.py` zurückgesetzt werden.

### Tab: Statistiken

- Gesamtanzahl von Scheiben, Serien und Einzeltreffern
- Aufschlüsselung nach Disziplin mit prozentualem Balkendiagramm
- Zeitpunkt des letzten Verbindungsaufbaus durch die Source-Komponente
- Zeitpunkt der letzten erfolgreichen Synchronisierung
- Zeitstempel der jüngsten Scheibe in der Datenbank
- Schaltfläche zum manuellen Aktualisieren der Anzeige

### Tab: Datenbank

- Alle Ergebnisse löschen: Löscht sämtliche Scheiben, Serien und Treffer (Sicherheitsabfrage erforderlich)
- Nach Disziplin löschen: Ergebnisse einer bestimmten Disziplin entfernen (Auswahl über Dropdown)
- Nach Starterliste löschen: Ergebnisse einer Starterliste entfernen (Auswahl über Dropdown)

Achtung Löschvorgänge können nicht rückgängig gemacht werden. Gelöschte Daten werden beim nächsten Sync-Lauf nicht automatisch wiederhergestellt, da der Source-Prozess nur geänderte Datensätze überträgt. Um alle Daten neu einzuspielen, muss ein vollständiger Sync erzwungen werden (z. B. durch Löschen der Datei `source.state` auf dem Source-Rechner).

### Vollständigen Sync erzwingen

Wenn nach einem Löschvorgang alle Daten erneut eingespielt werden sollen:

    # Auf dem Source-Rechner:
    sudo systemctl stop meyton-sync-source
    sudo rm /opt/meyton-sync/source/source.state
    sudo systemctl start meyton-sync-source

Beim nächsten Start erkennt der Source-Dienst das Fehlen der Statusdatei und überträgt alle Datensätze vollständig.