- Python 59.1%
- JavaScript 28.7%
- CSS 9.2%
- HTML 2.7%
- Dockerfile 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| .vscode | ||
| bruno | ||
| db | ||
| src | ||
| static | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| compose.yaml | ||
| Dockerfile | ||
| eintscheidungsbaum.drawio | ||
| main.py | ||
| Makefile | ||
| pipe:[503392] | ||
| pipe:[503393] | ||
| pipe:[818420] | ||
| PROBLEMS.md | ||
| pyproject.toml | ||
| README.md | ||
| ruff.toml | ||
| socket:[822586] | ||
| socket:[822588] | ||
| socket:[822590] | ||
| TODO.txt | ||
| uv.lock | ||
| VERSION | ||
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/teamvieweridals 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: Issueno_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 Issuesunmapped_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 (200started/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 nachphase1/phase2/phase3undissue_type, plus Zähl-MetadatenGET|POST|DELETE /api/v1/hide– Geräte ausblenden / wieder einblenden (Body: exakt eine vonninja_device_id/tanss_device_id)PATCH /api/v1/ninja/device/{id}– Ninja-Custom-Fields setzen (Body:tanssidund/oderteamviewerid)GET /api/v1/mapping/{entity}/options|list|pending– TANSS-Optionen, gespeicherte Mappings, offene Namen fürentity∈os|cpu|manufacturerPOST /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 zuordnenDELETE /api/v1/mapping/{entity}?ninja_name=…– Mapping entfernenGET /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-Wildcarddev.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 mitproxy_buffer_size 32k(+proxy_buffers 4 32k,proxy_busy_buffers_size 64k), weil die 302-Antwort des implicit-Flow-Logins beide JWTs imLocation-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/syncninjamit den Servicessyncninja(Imagegit.datajob.de/general/syncninja:latest) unddb(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.yamlläuft bei Merge-into-main(pull_request/closed, nur wenn merged) oder perworkflow_dispatch. Die Versions-Tags kommen aus derVERSION-Datei (aktuell1.0, FormatMAJOR.MINOR) und ergebengit.datajob.de/general/syncninja:<VERSION>plus:latestin der Forgejo-Container-Registry (Registry-Login mit dem SecretCI_TOKEN). - Dependency-Wheels:
tanss-api/ninja-apiwerden in den Schwestern-RepositoriesGENERAL/tanss-librarybzw.GENERAL/ninja-librarygebaut und veröffentlicht (.forgejo/workflows/publish.yaml, push-to-main/workflow_dispatch) – in dieselbeGENERAL-PyPI-Registry, aus deruv syncsie lädt. - Hash-Pinning:
uv.lockpinnt die Wheel-Hashes. Wird eine Library neu veröffentlicht (neue Wheel-Bytes bei gleicher Version), schlägtuv sync --frozenin 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).