No description
  • Python 59.1%
  • JavaScript 28.7%
  • CSS 9.2%
  • HTML 2.7%
  • Dockerfile 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-02 08:12:26 +00:00
.forgejo/workflows mass commit: todos, progress bar, endpoint refactoring 2026-08-24 11:17:08 +02:00
.vscode add debug file 2026-08-26 08:12:00 +02:00
bruno change port to 8060 2026-09-01 11:56:17 +02:00
db add mapping table backup 2026-09-02 15:57:48 +02:00
src jbin.01 2026-10-02 08:42:18 +02:00
static jbin.01 2026-10-02 08:42:18 +02:00
.dockerignore add docker image 2026-08-21 15:40:35 +02:00
.env.example docs: hygiene — TODO.txt abgeschlossen markieren, .env.example um LLM_MODEL/LLM_BATCH_SIZE ergänzen 2026-09-08 13:32:25 +02:00
.gitignore oss mapping 2026-08-27 16:55:56 +02:00
compose.yaml jbin.01 2026-10-02 08:42:18 +02:00
Dockerfile fix: install git in Dockerfile builder stage to allow git dependencies 2026-09-03 13:28:48 +00:00
eintscheidungsbaum.drawio add newissue types, headquarter, full-service validation 2026-08-27 08:28:33 +02:00
main.py jbin.01 2026-10-02 08:42:18 +02:00
Makefile wip 2026-08-21 07:27:33 +02:00
pipe:[503392] jbin.01 2026-10-02 08:42:18 +02:00
pipe:[503393] jbin.01 2026-10-02 08:42:18 +02:00
pipe:[818420] jbin.01 2026-10-02 08:42:18 +02:00
PROBLEMS.md docs: record 502 root cause (NPM proxy_buffer_size) + wildcard redirect fix 2026-09-08 11:05:53 +02:00
pyproject.toml point datajob index at GENERAL org package registry 2026-09-07 15:46:14 +02:00
README.md docs: README-Refresh — Auth (implicit flow), Deployment, CI/CD & Releases 2026-09-08 13:22:13 +02:00
ruff.toml init 2026-08-19 15:22:12 +02:00
socket:[822586] jbin.01 2026-10-02 08:42:18 +02:00
socket:[822588] jbin.01 2026-10-02 08:42:18 +02:00
socket:[822590] jbin.01 2026-10-02 08:42:18 +02:00
TODO.txt docs: hygiene — TODO.txt abgeschlossen markieren, .env.example um LLM_MODEL/LLM_BATCH_SIZE ergänzen 2026-09-08 13:32:25 +02:00
uv.lock refresh lock hashes: tanss-api/ninja-api now built by library CI 2026-09-07 16:34:19 +02:00
VERSION Add VERSION 2026-09-03 12:36:00 +00:00

Ninja-TANSS Sync Tool

syncninja vergleicht und synchronisiert die Geräte- und Kundendaten aus Ninja RMM (datajob.rmmservice.eu) mit der TANSS-Verwaltung (Geräte, Firmen, Verträge, Betriebssysteme, CPUs, System-Hersteller). Der Kern ist eine 3-Phasen-Pipeline, die Geräte abbildet, Verträge und Policies validiert und bereinigte Daten bidirektional in beide Systeme schreibt. Eine Web-UI (lieferbar aus dem Backend) zeigt die Ergebnis- und Problemübersicht in Echtzeit und erlaubt, KI-Vorschläge anzuwenden oder Geräte auszublenden.

Features

  • LLM-basiertes Geräte-Mapping: Ninja-Geräte ohne TANSS-Zuordnung werden per LLM (vLLM-Endpunkt, OpenAI-kompatibel) mit plausiblen TANSS-Geräten zusammengepasst. Die Vorschläge (mit Confidence und Begründung) sind in der UI anwendbar.
  • Live-Pipeline-Status: WebSocket-Streaming mit Fortschrittsanzeige, Timer und Snapshot+Cursor-Protokoll – mehrere Tabs/Benutzer erhalten denselben vollständigen Update-Stream, auch wenn sie während eines Laufs verbinden.
  • Apply- & Hide-UI: LLM-Vorschläge und Mappings direkt aus der UI anwenden (schreibt tanssid/teamviewerid als Ninja-Custom-Fields) oder Geräte aus den Ergebnissen ausblenden.
  • Write-back-Synchronisation: Phase 3 schreibt die bereinigten Werte (teamviewerId, osId, cpuTypeId, manufacturerId, cpuFrequency) zurück ins TANSS-Gerät, ein Patch nur, wenn es Abweichungen gibt – nie ein blindes Überschreiben.
  • Full-Service-Validierung: Verträge und Ninja-Policies von Full-Service-Kunden werden gegeneinander geprüft (aktiver Vertrag, Policy-Konformität, Online-Status, 90-Tage-Fristen).
  • Name-Mappings (OS / CPU / System-Hersteller): Ninja-Begriffe → TANSS-IDs über eigene Mapping-Tabellen mit UI (Dropdown + erstellen/zuordnen/entfernen), inkl. „pending"-Liste der noch ungelösten Namen.
  • Täglicher Auto-Lauf: Ein Daemon-Thread startet die Pipeline einmal täglich um 00:00 (Europe/Berlin).

Pipeline

Fetch: Ninja devices / organizations / policies
       TANSS devices / oss / cpus / manufacturers / companies / contracts
   │
   ▼
Phase 1  Device-Mapping Ninja → TANSS
         (Custom-Field tanssid, Display-IDs, LLM-Fallback für offene Geräte)
   │
   ▼
Phase 2  Validierung (Verträge, Policies, Online-Status)
   │
   ▼
Phase 3  Synchronisation (teamviewerId, osId, cpuTypeId, manufacturerId, cpuFrequency)

Phase 1 – Mapping (src/phases/phase1_mapping.py)

Jedes Ninja-Gerät wird über sein tanssid-Custom-Field und über Firmen-/Display-ID-Logik einem TANSS-Gerät zugeordnet. Unzugeordnete Geräte gehen in den LLM-Pfad: src/llm/connection.py bündelt sie in Batches (LLM_BATCH_SIZE, Default 25) und fragt das LLM parallel ab. Die Antwort pro Batch ist JSON mit TANSS-Gerät, Confidence (0.0–1.0) und Begründung. Ergebnis-Typen (Beispiele): ninja_org_not_found, tanss_device_not_found, company_mismatch, no_tanss_device_found, tanss_device_referenced_multiple_times, tanss_id_added (automatisch gesetztes Custom-Field).

Phase 2 – Validierung (src/phases/phase2_validation.py)

Prüft für jede zugeordnete Gerätepaarung: Existiert ein aktiver TANSS-Vertrag? Stimmt die Ninja-Policy zum Vertragsmodell (Rundum-sorglos / Sicherheits-Paket)? Ist das Gerät online – andernfalls greift bei nicht-Full-Service-Geräten eine 90-Tage-Gnadenfrist ab letztem Kontakt. Ergebnis-Typen (Beispiele): no_active_contracts, multiple_active_contracts, contract_mismatch, breach_of_contract, no_contact_90_days, incorrect_ninja_policy_for_full_service_contract.

Phase 3 – Synchronisation (src/phases/phase3_synchronization.py)

Für validierte Geräte werden die TANSS-Felder bereinigt und per PUT /pcs/{id} geschrieben:

  • teamviewerId – aus Ninja übernommen, fehlt dort: Issue no_teamviewer_id.
  • osId / cpuTypeId / manufacturerId – über die Mappings-Tabellen (os_mappings, cpu_mappings, manufacturer_mappings) exakt per Ninja-Namen aufgelöst. Ein Patch wird nur erzeugt, wenn das TANSS-Feld einen falschen oder keinen Wert hat (0/leer = unassigned in TANSS). Unaufgelöste Namen erzeugen die Issues unmapped_ninja_os / unmapped_ninja_cpu / unmapped_ninja_manufacturer.
  • cpuFrequency – aus Ninja in MHz übernommen.

Jede TANSS-Schreibung wird als Issue tanss_device_updated protokolliert, inkl. des exakten Patch-Body (changed) – die UI rendert das als „🔄 Geänderte Werte" (alt → neu).

Installation

Voraussetzungen: Python ≥ 3.14, uv und Zugriff auf das Paket-Registry von git.datajob.de.

Die generierten API-Clients tanss-api und ninja-api werden nicht mehr per Git-URL geladen, sondern aus dem Forgejo-Paket-Registry der Organisation GENERAL (PyPI-Simple-Index https://git.datajob.de/api/packages/GENERAL/pypi/simple). Der Index ist in pyproject.toml als [[tool.uv.index]] mit dem Namen datajob und explicit = true deklariert; [tool.uv.sources] weist tanss-api und ninja-api explizit auf diesen Index zu. Damit reicht der normale Befehl:

uv sync

Konfiguration

.env.example nach .env kopieren und anpassen:

Variable Beschreibung
NINJA_CLIENT_ID / NINJA_CLIENT_SECRET Ninja OAuth2-Client
NINJA_ACCESS_TOKEN_URL Token-URL, Default https://datajob.rmmservice.eu/ws/oauth/token
NINJA_API_BASE_URL Ninja RMM-Basis, Default https://datajob.rmmservice.eu
TANSS_WRAPPER_USERNAME / TANSS_WRAPPER_PASSWORD Basic-Auth für den TANSS Node-RED-Wrapper
TANSS_WRAPPER_API_BASE_URL Wrapper-Basis, Default https://nodered.dev.datajob.de
TANSS_DB_HOST / TANSS_DB_PORT / TANSS_DB_NAME / TANSS_DB_USERNAME / TANSS_DB_PASSWORD Direkte MySQL-Zugänge für TANSS-Verträge
MAX_WORKERS Thread-Pool-Größe für parallele API-Calls (Default 50)
LOG_LEVEL Logging-Level (Default INFO), Logs nach log.txt
DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_NAME PostgreSQL-Cache/DB des Tools
LLM_API_KEY / LLM_API_BASE vLLM/OpenAI-kompatibler Endpunkt für das LLM-Mapping
LLM_MODEL Modell-Name im LLM-Endpunkt (Default openai/DATAJOB/datallm)
LLM_MAX_TOKENS Max. Completion-Tokens pro LLM-Request (Default 8192)
LLM_BATCH_SIZE Ninja-Geräte pro LLM-Request (Default 25 – Reasoning-Modelle brauchen kleine Prompts)
KEYCLOAK_ENABLED Auth an/aus (Default true; false = offenes Local-Dev)
KEYCLOAK_URL / KEYCLOAK_REALM / KEYCLOAK_CLIENT_ID / KEYCLOAK_CLIENT_SECRET / KEYCLOAK_REQUIRED_ROLE Keycloak-Daten (Default https://auth.datajob.de / DATAJOB / syncninja / Rolle syncninja); Secret des Confidential-Client

Nur SYNCNINJA_PIPELINE_SCHEDULE=HH:MM (optional) und SYNCNINJA_PIPELINE_TZ (Default Europe/Berlin) steuern den täglichen Scheduler; es gibt bewusst keinen Nachhol-Lauf, wenn die App zum Zielzeitpunkt nicht erreichbar war.

Nutzung

CLI (einmaliger Lauf, ohne Web)

uv run main.py          # Pipeline mit PostgreSQL-Cache
uv run main.py --fresh  # Cache verwerfen, alle Daten frisch aus den APIs laden

Die CLI lädt alle Daten, druckt eine Datenübersicht (Anzahlen) und fährt die drei Phasen durch; das Ergebnis landet in log.txt.

Web (FastAPI + UI)

uv run main.py --serve   # http://localhost:8060
  • GET / – statische UI (static/, vanilla HTML/JS, same-origin)
  • POST /api/v1/pipeline – Pipeline-Lauf im Hintergrund starten (200 started / already_running)
  • WS /api/v1/ws/pipeline – Live-Status (Snapshot bei Verbindung, dann alle Updates)
  • GET /api/v1/states/invalid – alle Issues der letzten Phasen, gruppiert nach phase1/phase2/phase3 und issue_type, plus Zähl-Metadaten
  • GET|POST|DELETE /api/v1/hide – Geräte ausblenden / wieder einblenden (Body: exakt eine von ninja_device_id / tanss_device_id)
  • PATCH /api/v1/ninja/device/{id} – Ninja-Custom-Fields setzen (Body: tanssid und/oder teamviewerid)
  • GET /api/v1/mapping/{entity}/options|list|pending – TANSS-Optionen, gespeicherte Mappings, offene Namen für entity ∈ os|cpu|manufacturer
  • POST /api/v1/mapping/{entity} – Mapping zu einem bestehenden TANSS-Eintrag speichern (Body: ninja_name + tanss_name)
  • POST /api/v1/mapping/{entity}/create – TANSS-Eintrag neu anlegen und zuordnen
  • DELETE /api/v1/mapping/{entity}?ninja_name=… – Mapping entfernen
  • GET /api/v1/tanss-names – komplette ID→Name-Tabellen (os, cpu, manufacturer, company) zur Auflösung der Roh-IDs auf TANSS-Geräten

Docker / Docker Compose (lokale Entwicklung)

docker compose up -d --build

compose.yaml bringt drei Services: db (PostgreSQL, Port 5432), adminer (DB-UI, Port 8080) und syncninja (App, Port 8060, TZ=Europe/Berlin, Health-Check gegen GET /). Die App liest ihre Secrets aus .env; DB_HOST wird im Container auf db umgebogen. Im Produktivbetrieb wird das Image nicht lokal gebaut, sondern aus dem Registry gezogen (siehe „Produktivbetrieb & Deployment").

Produktivbetrieb & Deployment

  • Öfflich: https://syncninja.dev.datajob.de – DNS-Wildcard dev.datajob.de → 10.98.10.23; Terminierung über nginx-proxy-manager (NPM), vhost #58 mit Let's-Encrypt-Zert und WebSocket-Upgrade.
  • Keycloak: https://auth.datajob.de – NPM vhost #33 → Keycloak 26 auf 10.98.10.23:8443. Dieser vhost trägt eine per-Host-Advanced-Config mit proxy_buffer_size 32k (+ proxy_buffers 4 32k, proxy_busy_buffers_size 64k), weil die 302-Antwort des implicit-Flow-Logins beide JWTs im Location-Header transportiert (~10 KB > nginx-Default 8 KB) – ohne die Config 502 „upstream sent too big header" (PROBLEMS.md #7).
  • Host: 10.98.10.23, Compose-Projekt unter /root/syncninja mit den Services syncninja (Image git.datajob.de/general/syncninja:latest) und db (PostgreSQL auf persistentem Volume).
  • LAN-Direktzugang: http://10.98.10.23:8060.
  • Re-Deploy nach einem neuen Release:
cd /root/syncninja
sudo docker compose pull syncninja
sudo docker compose up -d

Die Auth- und 502-Historie (inkl. Diagnose) ist in PROBLEMS.md (Repository-Root) dokumentiert.

CI/CD & Releases

  • Container: .forgejo/workflows/container.yaml läuft bei Merge-into-main (pull_request/closed, nur wenn merged) oder per workflow_dispatch. Die Versions-Tags kommen aus der VERSION-Datei (aktuell 1.0, Format MAJOR.MINOR) und ergeben git.datajob.de/general/syncninja:<VERSION> plus :latest in der Forgejo-Container-Registry (Registry-Login mit dem Secret CI_TOKEN).
  • Dependency-Wheels: tanss-api/ninja-api werden in den Schwestern-Repositories GENERAL/tanss-library bzw. GENERAL/ninja-library gebaut und veröffentlicht (.forgejo/workflows/publish.yaml, push-to-main / workflow_dispatch) – in dieselbe GENERAL-PyPI-Registry, aus der uv sync sie lädt.
  • Hash-Pinning: uv.lock pinnt die Wheel-Hashes. Wird eine Library neu veröffentlicht (neue Wheel-Bytes bei gleicher Version), schlägt uv sync --frozen in der Dockerfile mit Hash-Mismatch fehl – Fix ist ein Lock-Refresh (vgl. PR #14 „refresh lock hashes for CI-built wheels").

Keycloak-Authentifizierung

Alle /api/v1/*-Endpunkte (REST und WebSocket) erfordern einen Keycloak-Access-Token mit der Realm-Rolle syncninja.

Client-Setup: Keycloak 26 auf https://auth.datajob.de, Realm DATAJOB, Client syncninja als Confidential-Client mit implicit Flow. Begründung: Keycloak 26 verbietet Service Accounts auf Public Clients, und der Browser kann kein Client-Secret halten – daher Confidential-Client + Implicit Flow (PROBLEMS.md #2). Die redirectUris/webOrigins des Clients sind auf exakte Werte gekürzt (2026-09-08, keine Wildcards): https://syncninja.dev.datajob.de/ und http://10.98.10.23:8060/.

Frontend: Die UI lädt keycloak-js 23.0.7 (UMD-Build, pinned, vom CDN – letzter UMD-Release) mit flow: 'implicit' und useNonce: false. Keycloak 26 verlangt bei implicit-Flow-Auth-Requests einen nonce, den keycloak-js nur bei useNonce: true mitsetzt – das würde aber gleichzeitig die Nonce auf allen Tokens validieren, während Keycloak sie nur im id_token mitliefert (Redirect-Loop). Die App monkey-patcht daher kc.createLoginUrl (der zentrale Funnel für alle Login-Redirects) und hängt nonce=<uuid4> an jede generierte Login-URL an; die Nonce wird im id_token zurückechoed, ohne dass das Client-Side validiert (PROBLEMS.md #3/#6). Unangemeldete Browser werden zum Login umgeleitet; der Token liegt in localStorage (stiller Refresh) und zusätzlich im WS-Cookie.

Server-seitig: fastapi-keycloak-middleware verifiziert stateless per RS256 gegen die Realm-JWKS (ein Key-Fetch beim Start, kein RTT pro Request); die Rollen-Prüfung (Realm-Rolle syncninja, Fallback auf Client-Role) läuft in der Middleware für beide Protokolle. Der WebSocket authentiziert sich über das Cookie access_token – dessen Wert ist die Authorization-Header-Form Bearer <token>.

KEYCLOAK_ENABLED=false in der .env schaltet die Auth komplett ab (lokale Entwicklung). Die Problem-/Entscheidungs-Historie zu Auth, Nonce und Gateway (502) steht in PROBLEMS.md (Repository-Root).

Projektstruktur

main.py                 CLI + API-Einstiegspunkt (uvicorn auf :8060)
src/
  pipeline.py           Fetch-Schritte + Phasen-Orchestrierung (emit_status-Callback)
  phases/               phase1_mapping / phase2_validation / phase3_synchronization
  llm/connection.py     LLM-Mapping (Batches, ThreadPool, JSON-Antwort)
  ninja/                generierter OpenAPI-Client + devices/organizations/policies, OAuth2
  tanss/                generierter OpenAPI-Client + devices/companies/contracts/oss/cpus/
                        manufacturers, MySQL-Zugriff (db.py)
  api/                  FastAPI-Router: routes, pipeline (WS+Hub), states, hide, mapping,
                        devices, scheduler (täglicher Lauf)
  db/connection.py      SQLAlchemy: Cache, HiddenDevice, Os/Cpu/ManufacturerMapping
  cache.py              PostgreSQL-JSON-Cache (fresh/cached Modus)
  models.py             Issue-Typen (Enum), Store, Schemas
  config.py             alle Env-Variablen
static/                 UI (index.html, app.js, style.css, keycloak.json)
bruno/                  Bruno-API-Collection (App / Ninja / Tanss)
compose.yaml            db + adminer + syncninja
Dockerfile              2-Stage-Image (uv sync --frozen → Runtime)
VERSION                 Release-Version für die Container-Tags (MAJOR.MINOR)
PROBLEMS.md             dokumentierte Problem-/Entscheidungs-Historie

Entwicklung

make fmt     # ruff check --fix + ruff format
make         # CLI-Pipeline
make serve   # Web-Server
make fresh   # CLI mit frischen API-Daten

Tests laufen ohne pytest-Framework als Headless-Skripte, z. B. uv run python tests/test_api_clients.py (API-Client-Import-Smoke-Test).