claude sync: 2026-07-02 10:59

This commit is contained in:
marcus.hinz
2026-07-02 10:59:36 +02:00
parent 3bb3beaa65
commit 41e74e9946
28 changed files with 1088943 additions and 83 deletions
+12
View File
@@ -0,0 +1,12 @@
{
"autoMemoryDirectory": "~/claude-projects/08-07/memory",
"permissions": {
"allow": [
"Edit(~/projects/08-07/**)",
"Edit(~/claude-projects/08-07/**)"
],
"additionalDirectories": [
"~/projects/08-07"
]
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"permissions": {
"allow": [
"Bash(python3 *)",
"Bash(cargo *)",
"Bash(npm *)",
"Bash(npx *)",
"WebSearch"
]
}
}
File diff suppressed because it is too large Load Diff
+294808
View File
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
- [Würth KI-Demo](project-wuerth-ki-demo.md) — Ziel + zwei Demo-Richtungen (Preis-/Margen-Assistent + Chat; Cross-Sell)
- [Quelldaten extracted_xml_data.json](reference-extracted-xml-data.md) — Struktur des Plato-Exports, 191 Aufträge
- [Tech-Stack](project-08-07-tech-stack.md) — Tauri (Rust-Backend + Vue) auf SQLite, LLM-Calls im Rust-Kern
- [Datenveredelung](project-08-07-datenveredelung.md) — Offline-Anreicherung: Branche, Geo, Dedup, Modul-Klartext; eigener Use Case
- [Datenmodell](project-08-07-datenmodell.md) — SQLite-Tabellen (auftraege/positionen/module/kunden) als Arbeitsschicht
- [Datenbasis](project-08-07-datenbasis.md) — 191 jetzt, Ziel ~2000; reicht für Demo, nicht für statistisches Cross-Sell
- [Fahrzeug-Veredelung](project-08-07-fahrzeug-veredelung.md) — Fzg-Typ nur in .plax-Rohdaten (PDF+Freitext), per LLM → auftrag_fzg; 134/191, fast alles Vans
- [Stückliste-Pipeline](project-08-07-stueckliste-pipeline.md) — Bauteil-Klartext+Artikelnr. aus den PDFs in den .plax; wiederholbarer Extract → module.klartext, Popup je Auftrag
- [AI-Assistent](project-08-07-ai-assistent.md) — Tab „Fragen": Text→SQL→Auto-Visualisierung via Claude (Sonnet 5); API-Key in ~/.ssh/anthropic_api_key
@@ -0,0 +1,18 @@
---
name: project-08-07-ai-assistent
description: Natursprache-Assistent (frage_ai) — Text→SQL→Auto-Visualisierung über Claude; API-Key liegt in ~/.ssh
metadata:
node_type: memory
type: project
originSessionId: 9605d7dd-3c40-4f92-b05f-540606518879
---
Tab **„Fragen"** in der App: Freitext-Frage → Rust-Command `frage_ai` schickt Frage + DB-Schema (aus `sqlite_master`) an die **Claude-API** (Modell `claude-sonnet-5`, `thinking:disabled`, max_tokens 1024) und lässt ein JSON zurückgeben: `{sql, chart(kpi|bar|table), label, value, titel, erklaerung}`. Rust prüft **read-only** (nur `SELECT`/`WITH`, kein `;`, keine DDL/DML) via `is_read_only`, führt die SQL auf `wuerth.db` aus und gibt Spalten+Zeilen zurück. Frontend rendert kpi/bar/table + zeigt die erzeugte SQL. Siehe [[project-08-07-tech-stack]].
**API-Key:** liegt **nicht** im Projekt/`.env`, sondern in **`~/.ssh/anthropic_api_key`** (chmod 600, eine Zeile `sk-ant-…`). `read_api_key()` liest den Pfad, nimmt die erste `sk-ant-`-Zeile. Getestet: gültig, API antwortet HTTP 200.
**Kosten** (Anthropic-API, separat vom Claude-Abo): Sonnet 5 ~0,01 $/Frage (Intro-Preis), Haiku 4.5 ~0,004 $/Frage → 5 $ reichen für hunderte Fragen. Das Abo ist dafür nicht nutzbar; eigener Console-Key nötig.
**Deps:** `reqwest` (rustls-tls, blocking) in `src-tauri/Cargo.toml`; Command läuft über `spawn_blocking`.
**End-to-End verifiziert:** „Umsatz je Gewerk" → bar auf `v_kunde_360`; „Gesamtumsatz in Euro" → kpi 609.861 €. Prompt-Hinweis: Kunden-Anzeigename = `name_roh` (nicht `name_1`).
+13
View File
@@ -0,0 +1,13 @@
---
name: project-08-07-datenbasis
description: Datenbasis-Bewertung — aktuell 191 Aufträge, Ziel ~2000; reicht für Demo, nicht für statistisches Cross-Sell
metadata:
type: project
---
Aktueller Export: **191 Aufträge / 152 Firmen**. Marcus versucht, **~2000** zu beschaffen.
Bewertung nach Zweck:
- **Demo** (Chat, Preis-/Margen-Assistent als Narrativ, Veredelung): 191 reichen locker — es zählen überzeugende Einzelfälle, keine Statistik.
- **Cross-Sell belastbar:** grenzwertig bei 191 (viele seltene/unikale Module, dünnes Ko-Vorkommen, Long Tail). Robuste „X→Y"-Regeln eher ab 1.0005.000 Aufträgen → daher das 2000-Ziel.
- **Klassisches ML-Margenmodell:** 191 zu wenig (Überanpassung bei ~25 Merkmalen). Deshalb LLM-basiertes Reasoning über ähnliche Fälle statt Training — funktioniert auch mit wenig Daten und ist für die Demo ohnehin der bessere Weg.
+18
View File
@@ -0,0 +1,18 @@
---
name: project-08-07-datenmodell
description: Strukturierte SQLite-Ablage als abgeleitete Arbeitsschicht; roher JSON bleibt Quelle
metadata:
type: project
---
Daten werden zusätzlich **strukturiert in SQLite** abgelegt (eine Datei, kein Setup). Der rohe JSON bleibt Single Source of Truth; SQLite ist die abgeleitete Arbeitsschicht.
**Why:** Cross-Sell braucht Aggregation über Module, Ko-Vorkommen und Joins mit der Veredelung — im verschachtelten JSON-Baum mühsam, als SQL trivial (group by). Anreicherung liegt pro Kunde einmal statt je Auftrag redundant. Demo-Abfragen werden deterministisch und schnell.
**Tabellen (How to apply):**
- `auftraege` — ein Satz pro Export: Kopf, Datum, Status, Margen, Kunden-Bezug.
- `positionen` — eine Zeile je Modul: auftrag_id, modul_id, Mengen, Preise (Basis der Warenkorbanalyse).
- `module` — Stammdaten: modul_id → Klartext, Kategorie (einmal, geteilt).
- `kunden` — dedupliziert, mit Branche/Größe/Geo aus der [[project-08-07-datenveredelung]].
Siehe [[project-08-07-datenbasis]] zur Mengenbewertung und [[reference-extracted-xml-data]] zur Quellstruktur.
@@ -0,0 +1,22 @@
---
name: project-08-07-datenveredelung
description: Offline-Batch-Anreicherung der Würth-Daten (Branche, Geo, Dedup, Modul-Klartext) — eigener verkäuflicher Use Case
metadata:
type: project
---
Die Daten werden **offline einmalig veredelt** (nicht live in der Demo), und die Demo läuft auf dem fertigen, sauberen Datensatz. Die Anreicherung ist selbst ein verkäuflicher Use Case: **Stammdaten-Veredelung / Data Quality** — vorhandene Kundendaten klassifizieren, vervollständigen, vereinheitlichen; anschlussfähig fürs CRM (Segmentierung, Zielgruppen, Cross-Sell).
**Branche zuordnen (dreistufig):**
1. LLM auf Firmenname — ~70 % eindeutig aus dem Namen (Elektro, Heizung/Sanitär/Klima, Holz/Zimmerei, Bau/Dach, Kfz/Autohaus, öffentlich/Stadtwerke, Fördertechnik).
2. Modulprofil aus `artikel_data_json` als Korrektur/Fallback für ~30 % generische Namen (Eigennamen, „Gloria", „SOS", „lean GmbH").
3. Web-Anreicherung mit **Name + PLZ** (offline, gecacht) — hebt Trefferquote auf ~100 %, liefert echte Branche, Größe, Gewerk; bestätigt Treffer über Firmensitz. Unsichere Matches als „unbestätigt" markieren, nicht raten.
**Weitere Veredelung:**
- Kunde: Dublettenauflösung + Kunden-ID (gleiche Firma mehrfach), Geo aus PLZ (Ort/Region/Koordinaten), Rechtsform normieren, Land vereinheitlichen, Segment (Handwerk/Industrie/Flotte/öffentlich).
- Fahrzeug: leere `fzg_*`-Felder aus Modulen/Radstand-Codes inferieren (Fahrzeugklasse, Aufbautyp).
- Artikel/Konfiguration (größter Hebel): `modul_id` → lesbarer Klartext + Kategorie (Boden, Regal, Befestigung, Montageset); Konfigurationsprofil-Label („Elektriker-Setup"); Komplexitätsmaß (Anzahl Komponenten).
- Preise/Kennzahlen: Datum aus Schlüssel als echtes Feld; Auftragswert-, Rabatt-, Margenklasse; Conversion-Label aus `quotation_state`.
- Bereinigung zuerst: HTML-Entities/Encoding, Whitespace; Konsistenz-Flags (Struktur ↔ Stückliste, Summen nachrechnen).
**Reihenfolge:** Bereinigung + Modul-Klartext → Kunde/Geo/Branche → abgeleitete Kennzahlen.
@@ -0,0 +1,18 @@
---
name: project-08-07-fahrzeug-veredelung
description: "Fahrzeugtyp steckt nur in den .plax-Rohdaten (PDF-Block + Freitext), nicht im extrahierten JSON; per LLM extrahiert nach auftrag_fzg"
metadata:
node_type: memory
type: project
originSessionId: a88dd1e6-722f-4188-bb2e-98574c0cf8d1
---
Die `fzg_*`-Felder im extrahierten JSON sind durchgängig leer; auch im PDF und XML sind die **strukturierten** Fahrzeugfelder (`FZG_MART_NR` etc.) leer. Der Fahrzeugtyp steckt aber in den `.plax`-Rohdaten unter `data/Auftragsplanungen/*.plax` (ZIP je Auftrag, Dateiname = `<auftrag_id>_<quotation_id>.plax`):
- **PDF-Block** „Fahrzeugbeschreibung" / „Kjøretøy beskrivelse" (155/191 vorhanden): bei DE-Aufträgen oft gefüllt mit Hersteller/Typ, Radstand+Länge (L1/L2/L3), Höhe (H1H3), Baujahr (z. B. „Ford Transit Custom, L1 H1").
- **Freitext** `subject` (storage.dict) + `INTERNER_TEXT` (großes XML im storage_dump): trägt den Typ bei NO-Aufträgen als Klartext (z. B. „Landcruiser"). Chaotisch: Tippfehler („Tranist", „Spritner"), gemischt DE/NO/FR/IT.
**Pipeline** (alle idempotent, nach [[project-08-07-datenmodell]]):
`collect_fzg_text.py` (braucht `pdftotext`/poppler) sammelt je Auftrag subject+interner Text+PDF-Block+fzg-Modulcodes → `fzg_texts.json` → 8 LLM-Agents extrahieren → `fzg_enrichment/result_*.json``enrich_fzg_load.py` füllt Tabelle `auftrag_fzg` (fzg_klasse, hersteller, modell, groesse, baujahr, konfidenz, quelle).
**Ergebnis:** 134/191 erkannt, 57 „unbekannt" (nicht geraten). Aufbau-Cluster `van|pickup|pkw|lkw|unbekannt`: fast alles **van** (133), 1 pickup, 0 pkw/lkw — der Aufbau-Cluster differenziert kaum; die **Größenklasse L1/L2/L3** trägt die eigentliche Unterscheidung. Frontend zeigt je Auftrag ein Cluster-Icon + Modell·Größe.
@@ -0,0 +1,29 @@
---
name: project-08-07-stueckliste-pipeline
description: Klartext-Stücklisten je Auftrag aus den eingebetteten PDFs der .plax ziehen — wiederholbare Pipeline für die kommenden ~2000 Aufträge
metadata:
node_type: memory
type: project
originSessionId: 9605d7dd-3c40-4f92-b05f-540606518879
---
Die Bauteil-Klartexte (Bezeichnung + Würth-Artikelnummer) stehen **nur in der eingebetteten PDF** jeder `.plax` (Auftragsbestätigung/Angebot), nicht in der XML/JSON — dort nur `modul_id` + Preise. Siehe [[reference-extracted-xml-data]], [[project-08-07-datenmodell]].
**Fundort in der .plax (ZIP):** `auftragsbestaetigung_*.pdf`. Stücklisten-Zeile: `Pos | Artikelnummer | Abmessungen | Menge | Nettowert`, darunter die Bezeichnungszeile, die mit dem Kurzzeichen (= `modul_id`) beginnt. Bei mehrsprachig-beschreibenden `modul_id` (z. B. `KlesknaggWürth4kroker`) ist die Zeile selbst die Bezeichnung mit Trennzeichen (`Klesknagg, Würth, 4 kroker`).
**Wiederholbare Pipeline (bei neuen .plax / für die ~2000 Aufträge):** `import.py` löscht `wuerth.db` und baut aus `schema.sql` **komplett neu** — danach müssen ALLE Folge-Loader wieder laufen, sonst sind Stückliste UND Veredelung leer. Reihenfolge:
1. Neue `.plax` nach `data/Auftragsplanungen/` legen; aktualisierten `data/extracted_xml_data.json` nach `data/`.
2. `python3 pipeline/import.py` — kunden/auftraege/module/positionen.
3. `python3 pipeline/extract_stueckliste.py` — PDFs → `module.klartext` + `module.artikelnummer`, Beleg `pipeline/stueckliste.json`.
4. `python3 pipeline/enrich_currency.py` — echte Währung + Wechselkurs (siehe unten) → `auftraege.waehrung/wechselkurs/kurs_datum`, Cache `pipeline/fx_cache.json`.
5. `python3 pipeline/enrich_load.py` — Firmendaten (`kunde_firmendaten`, `web_cache`) aus `pipeline/enrichment/*.json` (offline-Cache).
6. `python3 pipeline/enrich_fzg_load.py` — Fahrzeug (`auftrag_fzg`) aus `pipeline/fzg_enrichment/*.json`.
7. App gegen die DB; Popup je Auftrag rendert die Stückliste (`list_positionen`) inkl. EUR-Spalte + Kurs-Fußzeile.
Schritte 5/6 sind idempotent (leeren ihre Tabellen vorab) und laufen offline aus dem JSON-Cache — kein Web-Call.
**Währung/Wechselkurs (Schritt 4):** `extracted_xml_data.json`-Feld `currency` ist unbrauchbar (konstant `EUR` für alle). Echte Landeswährung nur im .plax-XML: `VARNAME:CURRENCY:USER:EINSTELLUNGEN_LAND` (EUR/NOK/CHF/PLN), bestätigt durch Label-Übersetzung `EUR→<Währung>`. Verteilung Stand 191: EUR 170, NOK 15, CHF 5, PLN 1. Kurs = EZB-Referenz zum Auftragsdatum (Abschlusstag aus dem Export-Key) via `api.frankfurter.dev` (User-Agent-Header nötig, sonst 403; wählt bei Wochenende/Feiertag den letzten Handelstag). `wechselkurs` = Einheiten je EUR (EUR=1), EUR-Wert = Betrag ÷ Kurs. `v_kunde_360.umsatz_netto` und `list_auftraege.umsatz_eur` sind darüber EUR-normiert (sonst NOK+EUR gemischt).
**Matching-Logik (extract_stueckliste.py):** Positionszeile per Split an ≥2 Leerzeichen (Spalten sauber trennen, sonst laufen Artikelnummer/Abmessung zusammen). Bezeichnung→`modul_id` über (a) erstes Token == modul_id, (b) normalisierter Präfix (Sonderzeichen/Leerzeichen strippen, lower) — fängt die ~10 % beschreibenden IDs, Abdeckung nahe 100 %. Kanonische Bezeichnung je Modul = häufigste, Deutsch bevorzugt (dieselbe modul_id hat je Auftrag DE/NO/IT-Varianten). Fallback `klartext = modul_id`, wenn ein Modul in keiner PDF vorkommt.
**Gemessen (Stand 213 .plax / 804 module):** 7.138 Positionszeilen, 90 % direkt per Kurzzeichen gemappt; Rest über Normalisierung.
+23
View File
@@ -0,0 +1,23 @@
---
name: project-08-07-tech-stack
description: Tech-Stack der Würth-KI-Demo — Tauri-Desktop-App (Rust-Backend + Vue-Frontend) auf SQLite
metadata:
type: project
---
Entscheidung: **Tauri-Desktop-App** statt separatem Web-Backend. Der Rust-Kern ist das Backend.
**Why:** Die LLM-Aufrufe brauchen den Claude-API-Key, der nicht in den Browser darf. Ohne Backend ginge nur SQLite-per-WASM (sql.js) + Vorberechnung — dann fällt der überzeugende Live-LLM-Teil weg oder der Key liegt offen. Tauri hält Key, SQLite und LLM-Calls im Rust-Kern, liefert eine Binary ohne Deploy — ideal für eine Vorführung.
**How to apply:**
- Frontend: Vue im Tauri-Webview (Vite + Vue, `create-tauri-app`).
- Rust-Backend als Tauri-Commands (über `invoke()` vom Frontend):
- `chat(frage)` → Kontext aus SQLite bauen, Claude-API via `reqwest` rufen, Antwort zurück; Key bleibt in Rust (.env/Tauri-Config).
- `query_orders`, `cross_sell(modul_id)`, `customer_segments` → feste SQL-Abfragen via `rusqlite`.
- Rust-Deps: `rusqlite`, `reqwest`, `serde`, `tokio`.
- Daten: veredelte SQLite-Datei als Tauri-Resource gebündelt.
- Veredelungs-Pipeline bleibt getrennt (Python), erzeugt nur die `.db`.
- Voraussetzung lokal: Rust-Toolchain + Node.
- App-Code soll in eigenes Projekt unter `~/projects/08-07` (analog Limbach: `~/claude-projects/08-07` = Memory/Steuerung, `~/projects/08-07` = Code).
Claude-Modelle (Stand 2026): Opus 4.8 `claude-opus-4-8`, Sonnet 4.6 `claude-sonnet-4-6`, Haiku 4.5 `claude-haiku-4-5-20251001`, Fable 5 `claude-fable-5`.
+14
View File
@@ -0,0 +1,14 @@
---
name: project-wuerth-ki-demo
description: KI-Demo für Würth auf Basis realer Angebots-/Auftragsdaten der Fahrzeugeinrichtung; zwei Demo-Richtungen
metadata:
type: project
---
Ziel: einem Unternehmen (Würth-Kontext) zeigen, was KI mit den vorhandenen Angebots-/Auftragsdaten aus dem Plato-Konfigurator leisten kann. Projektordner `~/claude-projects/08-07`, Quelldaten unter `data/` (siehe [[reference-extracted-xml-data]]).
Zwei Demo-Richtungen:
1. **Preis-/Margen-Assistent + Chat** — aus historischen Aufträgen (Einkaufspreis, Listenpreis, gewährter Rabatt, Marge RE/KA) ableiten, welcher Preis/Rabatt bei vergleichbarer Konfiguration üblich war; Margenwarnung; Ausreißererkennung. Kombiniert mit natürlichsprachiger Abfrage über den Bestand. LLM-basiert (Reasoning über ähnliche Fälle), kein klassisches ML-Training.
2. **Empfehlung / Cross-Sell** — Warenkorbanalyse über die Module (Ko-Vorkommen), Empfehlung fehlender Positionen; verstärkt durch Branche aus der Veredelung („Betriebe dieser Branche kaufen typischerweise auch …").
Voraussetzung für Richtung 2 ist die strukturierte Ablage ([[project-08-07-datenmodell]]) und die [[project-08-07-datenveredelung]]. Technik: [[project-08-07-tech-stack]]. Datenmengen-Bewertung: [[project-08-07-datenbasis]].
@@ -0,0 +1,17 @@
---
name: reference-extracted-xml-data
description: Struktur des Quell-Datensatzes data/extracted_xml_data.json — Plato-Export, 191 Aufträge
metadata:
type: reference
---
`data/extracted_xml_data.json` (≈18,6 MB). Top-Level-Dict, 191 Datensätze; Schlüssel `export_<Kürzel>_<JJJJMMTT>_<Nr>-1` (Kürzel = Bearbeiter/Kunde, Datum im Schlüssel). 152 eindeutige Firmen, einige Kunden mehrfach (z. B. Die Autobahn ×12, Elektrotechnik Schäfer ×7). Daten von 2024 bis 2026-06-24.
Pro Datensatz ~36 Felder, drei Blöcke + zwei verschachtelte JSON-Strukturen:
- **Kopf/Preise** `kopf_*`: einkaufspreis, summe_positionen_liste, summe_positionen, montage_zeit/-wert, fracht, rabatt_wert/-prozent, aufschlag_wert/-prozent, kundenpreis_netto, aw_wert, re, ka.
- **Fahrzeug** `fzg_*`: hersteller_id, typ, baujahr, dach, antrieb, leasing, rad_stand, seitentuer, wand — durchgängig leer, Kandidat zum Nachfüllen.
- **Kunde** `kde_*` + Meta: name, name_1/2, landeskenner, plz, strasse; currency, montage_art, quotation_state (z. B. `order`), wuerth_innen_landeskenner.
- **struktur_data_json**: Aufbau-/Geometriebaum (`root` mit Wänden rechts/links/stirn/heck/boden/dach, Bereich `ungeplant` mit Komponenten `COMP_xxxx`: pos/rot/skalierung, modul_id, platzierung, menge).
- **artikel_data_json**: Stückliste je `modul_id` mit einkaufspreis, preis_liste, preis, rabatt_wert/-modus/-max/-fix, aufschlag_wert, aw_wert, typ.
Datenhygiene: HTML-Entities (`&amp;`) und Unicode-Escapes auflösen; `landeskenner` uneinheitlich (`NO`/`Norway`, `DE`/`Deutschland`, teils leer); PLZ teils nur Ziffer ohne Ort; `modul_id` kryptisch und gemischtsprachig (de/no), z. B. `SMU73`, `PRGR2GULVMELLOMSTORVAREBILL1/L2/L3`, `KlesknaggWürth4kroker`.
-3
View File
@@ -14,9 +14,6 @@
"Bash(~/claude-projects/global/bin/git-sync:*)"
]
},
"env": {
"PATH": "/Users/marcus.hinz/.local/share/fnm/aliases/default/bin:/Users/marcus.hinz/.local/bin:${PATH}"
},
"enabledPlugins": {
"frontend-design@claude-plugins-official": true,
"swift-lsp@claude-plugins-official": true
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 435 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

+3 -1
View File
@@ -6,12 +6,14 @@
"Edit(~/projects/adesso-cpq/**)",
"Edit(~/projects/geko-infra/**)",
"Edit(~/projects/fire-visual/**)",
"Edit(~/projects/geko-fire/**)",
"Edit(~/claude-projects/robotunits/**)"
],
"additionalDirectories": [
"~/projects/geko-core",
"~/projects/adesso-cpq",
"~/projects/geko-infra"
"~/projects/geko-infra",
"~/projects/geko-fire"
]
},
"hooks": {
+286
View File
@@ -0,0 +1,286 @@
<!--
RSS generated by JIRA (1001.0.0-SNAPSHOT#100292-rev:5fda9e0fdd2f2fe2b6bc3376de24f51bf166a518) at Wed Jul 01 12:29:44 UTC 2026
It is possible to restrict the fields that are returned in this document by specifying the 'field' parameter in your request.
For example, to request only the issue key and summary add field=key&field=summary to the URL of your request.
-->
<rss version="0.92" >
<channel>
<title>Jira adesso Group extern</title>
<link>https://adesso-group-extern.atlassian.net</link>
<description>This file is an XML representation of an issue</description>
<language>en-us</language> <build-info>
<version>1001.0.0-SNAPSHOT</version>
<build-number>100292</build-number>
<build-date>30-06-2026</build-date>
</build-info>
<item>
<title>[GEKO-76] Allround-Schutzzaun: Intelligente Steher-Logik</title>
<link>https://adesso-group-extern.atlassian.net/browse/GEKO-76</link>
<project id="12341" key="GEKO">GEKO</project>
<description>&lt;p&gt;Dieses Ticket implementiert die selbstheilende Logik f&#252;r die Steher (Pfosten) des Allround-Schutzzauns. Ein platziertes Allround-Element wird automatisch in drei funktionsf&#228;hige 3D-Komponenten zerlegt. Die Steher passen ihre Bauform (A&#8211;G) nach dem Platzieren via lokalem Event-Listener dynamisch an belegte PMIs an und steuern ihre H&#246;he synchron zu den angeschlossenen Z&#228;unen. Nutzer k&#246;nnen Bauform und H&#246;he &#252;ber das Detailmen&#252; manuell &#252;berschreiben, wobei das UI die w&#228;hlbaren Bauformen streng auf die laut Matrix g&#252;ltigen Optionen einschr&#228;nkt.&lt;/p&gt;
&lt;h3&gt;&lt;a name=&quot;%F0%9F%9F%A6DefinitionofReady%C2%A0&quot;&gt;&lt;/a&gt;&lt;b&gt; Definition of Ready&lt;/b&gt;&#160;&lt;/h3&gt;
&lt;h4&gt;&lt;a name=&quot;Wer%3F%C2%A0&quot;&gt;&lt;/a&gt;&lt;b&gt;Wer?&lt;/b&gt;&#160;&lt;/h4&gt;
&lt;p&gt;Anwender des 3D-Viewings&lt;/p&gt;
&lt;h4&gt;&lt;a name=&quot;Was%3F%C2%A0&quot;&gt;&lt;/a&gt;&lt;b&gt;Was?&lt;/b&gt;&#160;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Automatische Aufspaltung eines Allround-Schutzzaun-Objekts in separate Steher- und Zaunknoten.&lt;/li&gt;
&lt;li&gt;Dynamischer In-Place-Austausch der Steher-Bauform (A&#8211;G) basierend auf belegten PMI-Winkeln.&lt;/li&gt;
&lt;li&gt;Automatische H&#246;hensteuerung des Stehers basierend auf dem Maximum der angeschlossenen Z&#228;une samt Fu&#223;freiheit.&lt;/li&gt;
&lt;li&gt;Implementierung einer fachlichen Ausnahme f&#252;r die L&#246;sch-Kaskadierung bei Schutzz&#228;unen.&lt;/li&gt;
&lt;li&gt;Manuelle Override-M&#246;glichkeit f&#252;r Bauform (dynamisch eingeschr&#228;nkt) und H&#246;he (nur Vergr&#246;&#223;erung) im Detailmen&#252;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;b&gt;Warum?&lt;/b&gt;&#160;&lt;/p&gt;
&lt;p&gt;Steher m&#252;ssen im 3D-Viewer als eigenst&#228;ndige Komponenten agieren, sich aber topologisch korrekt &quot;selbst heilen&quot; (Ecken/Kreuzungen bilden), um dem Nutzer manuelle Arbeit zu ersparen und fehlerfreie St&#252;cklisten zu garantieren.&lt;/p&gt;
&lt;h4&gt;&lt;a name=&quot;Wozu%3F%C2%A0&quot;&gt;&lt;/a&gt;&lt;b&gt;Wozu?&lt;/b&gt;&#160;&lt;/h4&gt;
&lt;p&gt;Gew&#228;hrleistung einer konsistenten Schutzzaun-Topologie im Szenegraphen.&lt;/p&gt;</description>
<environment></environment>
<key id="139414">GEKO-76</key>
<summary>Allround-Schutzzaun: Intelligente Steher-Logik</summary>
<type id="12871" iconUrl="https://adesso-group-extern.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium">Task</type>
<parent id="119259">GEKO-6</parent>
<priority id="10000" iconUrl="https://adesso-group-extern.atlassian.net/images/icons/priorities/critical.svg">Critical</priority>
<status id="14794" iconUrl="https://adesso-group-extern.atlassian.net/" description="">To Do</status>
<statusCategory id="2" key="new" colorName="blue-gray"/>
<resolution id="-1">Unresolved</resolution>
<assignee accountid="712020:1a31ac71-0e6f-49af-83f9-0d630a810204">Marcus Hinz</assignee>
<reporter accountid="712020:0a2bb4c7-7da6-48f0-8264-d021465e5f0f">Lukas H&#246;rger</reporter>
<labels>
</labels>
<created>Wed, 8 Apr 2026 16:57:23 +0200</created>
<updated>Wed, 1 Jul 2026 14:27:06 +0200</updated>
<due></due>
<votes>0</votes>
<watches>0</watches>
<attachments>
</attachments>
<subtasks>
</subtasks>
<customfields>
<customfield id="customfield_13434" key="com.atlassian.jira.plugin.system.customfieldtypes:textfield">
<customfieldname>Ansprechpartner</customfieldname>
<customfieldvalues>
<customfieldvalue>Lukas</customfieldvalue>
</customfieldvalues>
</customfield>
<customfield id="customfield_10000" key="com.atlassian.jira.plugins.jira-development-integration-plugin:devsummarycf">
<customfieldname>Development</customfieldname>
<customfieldvalues>
</customfieldvalues>
</customfield>
<customfield id="customfield_10019" key="com.pyxis.greenhopper.jira:gh-lexo-rank">
<customfieldname>Rank</customfieldname>
<customfieldvalues>
<customfieldvalue>0|i5ji9e:</customfieldvalue>
</customfieldvalues>
</customfield>
<customfield id="customfield_13432" key="com.atlassian.jira.plugin.system.customfieldtypes:select">
<customfieldname>T-Shirt Size - Storypoint</customfieldname>
<customfieldvalues>
<customfieldvalue key="20364"><![CDATA[M]]></customfieldvalue>
</customfieldvalues>
</customfield>
<customfield id="customfield_13430" key="com.atlassian.jira.plugin.system.customfieldtypes:textarea">
<customfieldname> Definition of Ready - Formale Kriterien</customfieldname>
<customfieldvalues>
<customfieldvalue>&lt;h3&gt;&lt;a name=&quot;%F0%9F%93%98Funktionsbeschreibung%3A&quot;&gt;&lt;/a&gt;&lt;b&gt; Funktionsbeschreibung:&lt;/b&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Input:&lt;/b&gt; Platzierungs-, &#196;nderungs- oder L&#246;sch-Events von Allround-Schutzzaunkomponenten im Viewport, sowie manuelle Eingaben im Detailmen&#252;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Verhalten / Ablauf:&lt;/b&gt;
&lt;ol&gt;
&lt;li&gt;&lt;b&gt;Aufspaltung bei Erst-Einbau (Typ 1):&lt;/b&gt; Wird ein Allround-Element frei platziert, splittet die Engine dieses in drei 3D-Komponenten: &lt;tt&gt;Vorderer Steher&lt;/tt&gt; -&amp;gt; &lt;tt&gt;Zaunelement&lt;/tt&gt; -&amp;gt; &lt;tt&gt;Hinterer Steher&lt;/tt&gt;. Der vordere Steher ist die Wurzel (Weltkoordinaten/Typ 1). Zaun und hinterer Steher h&#228;ngen kaskadierend via PMI (Typ 2) daran.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Anbau von Folge-Elementen (Typ 2):&lt;/b&gt; Snappt ein neues Element an das Ende eines bestehenden Zauns, wird der hintere Steher des Vorg&#228;ngers zum gemeinsamen &lt;em&gt;Verbindungssteher&lt;/em&gt;. Das neue Zaunelement h&#228;ngt an dessen PMI.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Selbstheilende Bauform-Aktualisierung:&lt;/b&gt; Nach dem Mouse-Up-Event pr&#252;ft ein lokales Callback die belegten PMI-Seiten des Stehers. Anhand einer exakten Matrix &lt;em&gt;(siehe separater Algorithmus-Abschnitt unten)&lt;/em&gt; wird das 3D-Modell in-place gegen die passende Bauform (A-G) ausgetauscht.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;H&#246;hen-Synchronisation &amp;amp; Metadaten:&lt;/b&gt; Die Standardh&#246;he ist 2200 mm. Die Steherh&#246;he folgt automatisch dem Maximum aller verbundenen Z&#228;une: &lt;tt&gt;max(Zaunh&#246;he 1 + Fu&#223;freiheit 1, Zaunh&#246;he 2 + Fu&#223;freiheit 2, ...)&lt;/tt&gt;. Jeder Steher f&#252;hrt pro Zaun den unteren Startpunkt (Fu&#223;freiheit) und das obere Ende als Metadatum mit. Weicht die H&#246;he von 2200 mm ab, generiert GEKO einen &lt;em&gt;Sondersteher&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Manuelles Override (Detailmen&#252;):&lt;/b&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;H&#246;he:&lt;/em&gt; Der Nutzer kann die H&#246;he manuell vergr&#246;&#223;ern. Ein Verkleinern unter das berechnete Minimum wird blockiert.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Bauform:&lt;/em&gt; Der Nutzer kann die automatisch berechnete Bauform im Detailmen&#252; manuell &#252;berschreiben. &lt;b&gt;Wichtig f&#252;r die UI-Logik:&lt;/b&gt; Das Auswahl-Dropdown darf dem Nutzer dynamisch nur die Bauformen (A-G) anbieten, die f&#252;r die aktuelle &quot;Eigene Anbau-Seite&quot; (STARTPUNKT, L oder R) laut der Ermittlungs-Matrix &#252;berhaupt zul&#228;ssig sind. Ist ein Steher beispielsweise auf Anbau-Seite &lt;tt&gt;L&lt;/tt&gt; eingebaut, m&#252;ssen die Optionen &lt;tt&gt;C&lt;/tt&gt; und &lt;tt&gt;E&lt;/tt&gt; im Men&#252; ausgeblendet oder deaktiviert werden.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Kaskadierendes L&#246;schverhalten (Sonderregel):&lt;/b&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Steher l&#246;schen:&lt;/em&gt; Alle direkt verbundenen Z&#228;une werden mitgel&#246;scht.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Zaun l&#246;schen:&lt;/em&gt; Angrenzende Steher ohne weitere Z&#228;une werden mitgel&#246;scht. Angrenzende Steher mit weiteren Z&#228;unen bleiben stehen und aktualisieren ihre Bauform.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;a name=&quot;Algorithmus%3ASteherBauformermitteln%28%7B%7Blayouttauschensteher%7D%7D%29&quot;&gt;&lt;/a&gt;Algorithmus: Steher-Bauform ermitteln (&lt;tt&gt;layout_tauschen_steher&lt;/tt&gt;)&lt;/h4&gt;
&lt;p&gt;Die Engine ermittelt f&#252;r jeden Steher die Seite des Ankers (ist er selbst am Startpunkt/Typ 1 Platzierung, an der linken (L) oder rechten (R) Seite eines Elements angebaut?) und liest die Winkel der aktuell an ihm belegten PMIs (0&#176;, 90&#176;, 180&#176;). Daraus ergibt sich folgende Matrix zur Bestimmung der Bauform-Artikelnummer (A-G):&lt;/p&gt;
&lt;div class=&apos;table-wrap&apos;&gt;
&lt;table class=&apos;confluenceTable&apos;&gt;&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;Eigene Anbau-Seite&lt;/b&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;Belegte PMIs am Steher&lt;/b&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;Resultierende Bauform&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;STARTPUNKT&lt;/b&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;tt&gt;0&lt;/tt&gt; und &lt;tt&gt;180&lt;/tt&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;A&lt;/b&gt; (ana)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;STARTPUNKT&lt;/b&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;tt&gt;180&lt;/tt&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;B&lt;/b&gt; (bna)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;STARTPUNKT&lt;/b&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;tt&gt;0&lt;/tt&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;C&lt;/b&gt; (cna)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;STARTPUNKT&lt;/b&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;tt&gt;90&lt;/tt&gt; und &lt;tt&gt;180&lt;/tt&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;D&lt;/b&gt; (dna)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;STARTPUNKT&lt;/b&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;tt&gt;0&lt;/tt&gt; und &lt;tt&gt;90&lt;/tt&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;E&lt;/b&gt; (ena)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;STARTPUNKT&lt;/b&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;tt&gt;0&lt;/tt&gt;, &lt;tt&gt;90&lt;/tt&gt; und &lt;tt&gt;180&lt;/tt&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;F&lt;/b&gt; (fna)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;STARTPUNKT&lt;/b&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;em&gt;Sonstige&lt;/em&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;G&lt;/b&gt; (gna)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;L&lt;/b&gt; (Links)&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;em&gt;(keine)&lt;/em&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;B&lt;/b&gt; (bna)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;L&lt;/b&gt; (Links)&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;tt&gt;0&lt;/tt&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;A&lt;/b&gt; (ana)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;L&lt;/b&gt; (Links)&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;tt&gt;90&lt;/tt&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;D&lt;/b&gt; (dna)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;L&lt;/b&gt; (Links)&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;tt&gt;0&lt;/tt&gt; und &lt;tt&gt;90&lt;/tt&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;F&lt;/b&gt; (fna)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;L&lt;/b&gt; (Links)&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;em&gt;Sonstige&lt;/em&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;G&lt;/b&gt; (gna)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;R&lt;/b&gt; (Rechts)&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;em&gt;(keine)&lt;/em&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;C&lt;/b&gt; (cna)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;R&lt;/b&gt; (Rechts)&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;tt&gt;180&lt;/tt&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;A&lt;/b&gt; (ana)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;R&lt;/b&gt; (Rechts)&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;tt&gt;90&lt;/tt&gt; und &lt;tt&gt;180&lt;/tt&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;F&lt;/b&gt; (fna)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;R&lt;/b&gt; (Rechts)&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;em&gt;Sonstige&lt;/em&gt;&lt;/td&gt;
&lt;td class=&apos;confluenceTd&apos;&gt;&lt;b&gt;G&lt;/b&gt; (gna)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;h4&gt;&lt;a name=&quot;NichtZiele%28OutofScope%29&quot;&gt;&lt;/a&gt;Nicht-Ziele (Out of Scope)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;Echtzeit-Austausch w&#228;hrend des Ziehens:&lt;/b&gt; Die Bauform- und H&#246;henberechnung triggert niemals w&#228;hrend der Mausbewegung, sondern exakt nach dem Mouse-Up-Event oder nach Best&#228;tigung im Detailmen&#252;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;a name=&quot;%F0%9F%93%91Akzeptanzkriterien%3A&quot;&gt;&lt;/a&gt;&lt;b&gt; Akzeptanzkriterien:&lt;/b&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Jedes Allround-Zaunelement wird beim Platzieren in separat selektierbare Steher- und Zaunkomponenten aufgeteilt.&lt;/li&gt;
&lt;li&gt;Beim sequentiellen Anbau teilen sich zwei Zaunelemente exakt einen gemeinsamen Verbindungssteher.&lt;/li&gt;
&lt;li&gt;Die Steher-Bauform heilt sich nach dem Mouse-Up gem&#228;&#223; der Matrix (A-G) selbst.&lt;/li&gt;
&lt;li&gt;Im Detailmen&#252; kann der Nutzer die Bauform manuell &#252;berschreiben. &lt;b&gt;Das UI schr&#228;nkt die Auswahlm&#246;glichkeiten hierbei strikt und dynamisch auf die f&#252;r diese Anbau-Seite g&#252;ltigen Zeilen der Matrix ein.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Die Steherh&#246;he entspricht mindestens dem Maximum aus &lt;tt&gt;Zaunh&#246;he + Fu&#223;freiheit&lt;/tt&gt;.&lt;/li&gt;
&lt;li&gt;Manuelle H&#246;hen-Eingaben unter das Minimum werden im UI blockiert.&lt;/li&gt;
&lt;li&gt;Sonderregel L&#246;schen (Steher): L&#246;schen eines Stehers l&#246;scht angebundene Z&#228;une.&lt;/li&gt;
&lt;li&gt;Sonderregel L&#246;schen (Zaun): L&#246;schen eines Zauns l&#246;scht alleinstehende Steher; verbundene Steher bleiben stehen und passen Bauform an.&lt;/li&gt;
&lt;li&gt;Sonderregel L&#246;schen (Kaskade): L&#246;schen eines Stehers f&#252;hrt zur L&#246;schung der angebundenen Z&#228;une. L&#246;schung eines angebundenen Zauns f&#252;hrt, falls vorhanden, zur L&#246;schung alleinstehender Steher&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;a name=&quot;%F0%9F%A7%AATestCases%3A&quot;&gt;&lt;/a&gt;&lt;b&gt; Test Cases:&lt;/b&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;&lt;span class=&quot;error&quot;&gt;&amp;#91;L&#246;sch-Kettenabbruch&amp;#93;&lt;/span&gt;&lt;/b&gt;: Strang &lt;tt&gt;Steher A -&amp;gt; Zaun 1 -&amp;gt; Steher B -&amp;gt; Zaun 2 -&amp;gt; Steher C&lt;/tt&gt; aufbauen -&amp;gt; &lt;tt&gt;Steher A&lt;/tt&gt; l&#246;schen -&amp;gt; &lt;em&gt;Ergebnis:&lt;/em&gt; &lt;tt&gt;Zaun 1&lt;/tt&gt; wird gel&#246;scht. &lt;tt&gt;Steher B&lt;/tt&gt; bleibt stehen (da &lt;tt&gt;Zaun 2&lt;/tt&gt; angebunden) und wechselt Bauform. &lt;tt&gt;Zaun 2&lt;/tt&gt; und &lt;tt&gt;Steher C&lt;/tt&gt; bleiben unber&#252;hrt.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span class=&quot;error&quot;&gt;&amp;#91;Bauform-Selbstheilung bei Eck-Anbau&amp;#93;&lt;/span&gt;&lt;/b&gt;: Geraden Zaun platzieren (Verbindungssteher ist Bauform A) -&amp;gt; Neues Zaunelement im 90&#176;-Winkel anbauen -&amp;gt; &lt;em&gt;Ergebnis:&lt;/em&gt; Nach Mouse-Up wechselt der Verbindungssteher sofort in die passende Eck-Bauform laut Matrix.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span class=&quot;error&quot;&gt;&amp;#91;Manuelle Override-Tests &amp;amp; UI-Restriktion&amp;#93;&lt;/span&gt;&lt;/b&gt;: Folge-Zaunelement anbauen (Verbindungssteher befindet sich auf Anbau-Seite &lt;tt&gt;L&lt;/tt&gt;). Verbindungssteher selektieren und Detailmen&#252; &#246;ffnen -&amp;gt; &lt;em&gt;Ergebnis:&lt;/em&gt; Das Dropdown f&#252;r die Bauform bietet nur A, B, D, F und G an. Die Optionen C und E sind nicht w&#228;hlbar oder ausgeblendet. Der Wechsel auf eine zul&#228;ssige Bauform (z. B. F) aktualisiert das 3D-Modell sofort.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;&lt;a name=&quot;%E2%9A%A0%EF%B8%8FEdgeCases%3A%C2%A0&quot;&gt;&lt;/a&gt;&lt;b&gt;&#9888;&#65039; Edge Cases:&lt;/b&gt;&#160;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;&lt;span class=&quot;error&quot;&gt;&amp;#91;Misch-H&#246;hen an einem Steher&amp;#93;&lt;/span&gt;&lt;/b&gt;: Z&#228;une mit H&#246;hen 2500 mm und 2200 mm treffen sich -&amp;gt; &lt;em&gt;Verhalten:&lt;/em&gt; Steher &#252;bernimmt die maximale H&#246;he und wird zum Sondersteher.&lt;/li&gt;
&lt;/ul&gt;
</customfieldvalue>
</customfieldvalues>
</customfield>
<customfield id="customfield_13429" key="com.atlassian.jira.plugin.system.customfieldtypes:multicheckboxes">
<customfieldname> DoR - Checkliste</customfieldname>
<customfieldvalues>
<customfieldvalue key="20347"><![CDATA[Ziel eindeutig formuliert (W-Fragen vollständig?)]]></customfieldvalue>
<customfieldvalue key="20348"><![CDATA[Ansprechpartner benannt]]></customfieldvalue>
<customfieldvalue key="20349"><![CDATA[Akzeptanzkriterien definiert]]></customfieldvalue>
<customfieldvalue key="20350"><![CDATA[mind. ein Testfall dokumentiert]]></customfieldvalue>
<customfieldvalue key="20351"><![CDATA[Priorität gesetzt]]></customfieldvalue>
<customfieldvalue key="20352"><![CDATA[Edge Cases beschrieben]]></customfieldvalue>
</customfieldvalues>
</customfield>
</customfields>
</item>
</channel>
</rss>
+1 -79
View File
@@ -1,81 +1,3 @@
# Offene Punkte — bei jedem Start prüfen und abhaken
Reihenfolge: Paul · Marcel (fire-visual / Bundle) · cpq.
---
## Paul
### [ ] Schutzzaun „Element" kaputt
- **Wo:** `adesso-cpq/packages/frontend/src/components/building-blocks/ProductBrowser.vue`, Filterzeile (TODO (Paul)).
- **Stand:** Kataloggruppe `Element` im `schutzzaun_basic` per Filter rausgenommen.
- **Nötig:** Element richtig fixen statt ausfiltern.
---
## Marcel — cpq-Selection
### [ ] Selection-Logik
- **Wo:** `adesso-cpq/packages/frontend/src/components/visual/DefaultCommandProzessor.ts`, Events `selection` / `selection-click` (FIXME (Marcel)).
- **Nötig:** Selection-Logik sauber in den Griff bekommen.
---
## Marcel — Scale
### [ ] Scale-Hack vs. PlacementMode — wie lösen?
- **Hack:** `Fire3D.vue` skaliert per `scaleScene` jedes Root-Handle zur Laufzeit mit `0.002`.
- **Konflikt:** PlacementMode überschreibt die Subject-Matrix mit `Vector3.one` (`subject.matrix = bestMatrix · dragPointTransform⁻¹`). DragPoint-Transform trägt `S`, sein Inverses `1/S`, Subject bekommt `1/S` → Geometrie netto `S·1/S = 1` → ~500×.
- **Folge:** Jeder einheitliche Laufzeit-Scale (Root, Wrapper, „kleine" Selection) überlebt den Snap nicht. Hack und PlacementMode sind auf demselben Subject unvereinbar.
- **Einzige überlebende Lösung:** `S=1`, also reale kleine Koordinaten in den Daten selbst — Geometrie **und** PMIs konsistent, kein Laufzeit-Scale — plus Kamera-Config (`camera-controller` `defaultPosition`/`fixPoint`, Ortho-Frustum auf mm).
- **Offen/ungeprüft:** Ob fire-visual einen vorgesehenen Mechanismus hat (Unit-System `unitConverter`/`environment.units`; FBX `UnitScaleFactor` greift bisher nur beim Import, nicht auf fertige JSON-SceneGraphen).
- **Entscheidung nötig:** Daten in realer Einheit liefern vs. Lib-Eingriff (PlacementMode behält Subject-Scale).
---
## Marcel (Montag) — fire-visual
### [ ] Lib-Bug movementMode — an fire-visual melden
- **Wo:** `fire-visual` `threed/interaction/module/src/interactionMode/movementMode.ts`, `updateArrowHighlight`.
- **Bug:** Z. 295299 greift `(this.interactionHelperHandle.find('.xy-plane') as Handle).renderer…` **ohne** null-Prüfung — die Achsen (Z. 305318) sind dagegen abgesichert (`if (!arrowElement) return; if (arrowHandle?.renderer)`).
- **Trigger:** Plane-Kind fehlt (Gizmo-Helper existiert, Planes weg — z. B. wenn ein voller `setScene` das Gizmo bei aktivem Move abräumt) → `Cannot read properties of null (reading 'renderer')` in **jedem rAF-Frame** → kompletter Freeze.
- **Fix:** Upstream — Z. 295299 konsistent zu Z. 305318 absichern.
- **Stand:** Cpq-seitig vorerst umschifft, Upstream-Fix trotzdem nötig.
### [ ] Funktionierendes Bundle (`@ad/fire` mit Relations-Resolver)
- **Ist:** cpq lädt `@ad/fire@1.0.5` (gebündelt in `packages/runtime/visual` via `tsup`, gelinkt nach `packages/frontend/node_modules/@ad-cpq/visual-runtime`).
- **Lücke:** 1.0.5 hat `RelationsFeature` + `createRelation`, aber **nicht** `resolveRelations` / Command `resolve_relations` (im Bundle 0 Vorkommen) — der Resolver kam in der fire-visual-Quelle (`~/projects/fire-visual`) erst nach 1.0.5 dazu.
- **Folge:** `attachment_relations` (mit `constraint_position`) wird beim Placement zwar geschrieben und die Relation instanziiert, bleibt aber **inert** (kein Resolver, kein Trigger) → verbundene Teile ziehen beim Move nicht mit.
- **Nötig:** `@ad/fire` auf Stand mit Resolver bringen — fire-visual lokal bauen (`build:local`, yarn-workspaces-Monorepo) → neues `@ad/fire`, cpq's `@ad/fire@1.0.5` darauf umbiegen (pnpm-override / `file:`-Link), `packages/runtime/visual` neu bauen. Achtung API-Drift neuere fire-visual vs. cpq/Runtime.
- **Zusätzlich offen, sobald Resolver da:**
1. **Auto-Trigger fehlt auch in der Quelle**`resolveRelations` nur via Command, kein Update-Hook/Transform-Abo; sauber wäre der Trigger im Move-/Placement-Mode der Engine (mit gezogenem Teil gepinnt), sonst muss cpq es von außen treiben.
2. **Persistenz** — aufgelöste Nachbar-Transforms leben in der Engine, nicht im cpq-eventStore → Readback (`get_transformation`) nötig, sonst Rücksprung beim nächsten `setScene`.
### [ ] Sichtbarer Gap beim Verbau-Ziehen
- **Symptom:** Beim Ziehen eines verbundenen Teils hängen die Follower sichtbar hinterher.
- **Ursache:** `resolutionScheduler` löst **deferred** auf — das gegriffene Teil bewegt der Gizmo in der Input/Update-Phase desselben Frames, die Relationen werden nur **markiert** (`_onTransformChange`) und erst in der **Process-Phase** (`_resolveNow`) aufgelöst, also einen Schritt später.
- **Effekt:** Follower trailen konstant um eine Frame-Bewegung (Gap skaliert mit Ziehgeschwindigkeit; bei Stillstand holen sie im nächsten Frame auf → „im Block"). Kein Timer/Throttle, `MAX_ITERATIONS=10`.
- **Erwartung:** fire-visual nimmt den Gap raus (Auflösung synchron zum Move) **oder** animiert das Nachziehen sauber.
- **Stand:** Cpq-seitig nicht lösbar ohne den Resolver gegen die Lib synchron zu treiben.
### [ ] Null-Matrix-Crash beim `setScene`-Teardown (Relationen + Rotation)
- **Symptom:** Nach mehreren Rotationen verbundener Teile (jede über cpq-`setScene` → Rebuild): `Uncaught Error: Matrix4_4(...zeros...) - inverse: can't be calculated` in `VolumeContext.updateWTL``ColliderComponent.syncTransformation``SynchronizeQueue.process` ← rAF, **jeden Frame** → Freeze. Davor flutet der Log mit `FeatureComponent - deregister` (Teardown der alten Szene).
- **Ursache:** Ein Collider synchronisiert während/nach dem `setScene`-Teardown gegen eine bereits abgeräumte/**genullte** Transform → Null-Matrix → Inverse unmöglich.
- **Repro:** Aktive `attachment_relations` (Position+Rotation) + wiederholtes `setScene`; kumulativ (nach einigen Rebuilds, gleiche Transform-Paarung).
- **Ausgeschlossen:** **Nicht** das Quaternion-Format (`values` = `[w,x,y,z]`, verifiziert), **nicht** Resolver/`localA`/`constraint_rotation` (mit vollständigem Constraint-Satz unverändert). Während des Ziehens (Engine-Live-Resolve) alles korrekt — nur der cpq-`setScene`-Rebuild beim Loslassen triggert es.
- **Einordnung:** Gleiche Klasse wie der movementMode-null-renderer-Freeze — Lib räumt beim Rebuild nicht sauber ab bzw. Collider-Sync läuft gegen tote Transform. Upstream-Fix nötig.
- **Stand:** Cpq-seitig umschifft — `handleRotateEnd` macht **kein** `setScene` mehr (Engine hat den Rotations-Endzustand live korrekt; Event wird nur recorded).
- **Offen:** Ein **nachfolgendes** `setScene` (z. B. Placement nach Rotation) replayt die Rotation und könnte denselben Crash auslösen. Eigentlicher Fix: upstream oder **inkrementelles** Transform-Update (`set_transformation`) statt Full-`setScene` für Move/Rotate.
---
## cpq
### [ ] Verbau-Code refactoren (nach den Klärungen)
- **Wo:** `DefaultCommandProzessor` / `SceneReplayProcessor` / `EventStore` / `eventDataUtil`.
- **Gewachsen über die Klärungsrunden (A-Modell):**
- Move-Ende recorded bewegtes Teil + transitive Follower als eine `pushGroup`-Gruppe.
- Placement schreibt EIN gemeinsames `attachment_relations` (wegen Clobber im `relationsProcessor`).
- Position-Korrektur `pos = event.position dragPMI-Offset`.
- Subject-Ausschluss in DropPoints via `:not`.
- **Stand:** Funktioniert (Kette ≥4, Undo/Redo gruppiert), aber aufräumen.
Keine offenen Punkte.
Binary file not shown.
+1
View File
@@ -8,3 +8,4 @@
- [cpq Storybook starten](cpq-storybook-start.md) — pnpm --filter @ad-cpq/common-frontend storybook (Port 6006)
- [Git nur Fast-Forward](feedback-git-nur-fast-forward.md) — kein rebase/merge/force; bei Divergenz melden statt auflösen
- [fire-visual Placement-Mode](fire-visual-placement-mode.md) — snap-to-point: DragPoints/DropPoints, processInput-Algorithmus, start_placement_mode-Command, PMI-DropPoints
- [cpq Verbau 90° am Steher](cpq-verbau-90grad-steher.md) — Engine kann's, cpq-GroupProcessor (translation-only) plättet die Rotation beim Replay; Punktdaten tragen die Orientierung schon
@@ -0,0 +1,28 @@
---
name: cpq-verbau-90grad-steher
description: "Ziel 90°-Anbau am Steher wie jetzt Links/Rechts — Engine kann es, cpq-GroupProcessor (translation-only) ist der Blocker"
metadata:
node_type: memory
type: project
originSessionId: e3d54daa-eb88-474f-9e99-62bfe83821b2
---
Ziel: an den Stehern im 90°-Winkel anbauen können, genau wie der jetzige Links/Rechts-Verbau.
**Wer ist der Blocker: wir (cpq), nicht fire-visual.**
- **fire-visual kann es.** Das Live-Snapping im PlacementMode setzt die Subject-Matrix als `bestMatrix · dragPointTransform⁻¹` — die Rotation des Drag-Punkts ist drin, die Engine richtet also den vollen Frame aus und dreht das Teil beim Snap schon korrekt um 90°. (Vorbehalt: aus der Placement-Matrix-Notiz abgeleitet, nicht aus frischem Quell-Read; bei Bedarf `PlacementMode`-Quelle prüfen, ob DropPoints den Rotations-Frame durchreichen.)
- **cpq verliert es beim Replay.** Nach dem Snap übernehmen wir das Engine-Ergebnis nicht, sondern merken nur „Punkt A trifft Punkt B" (`group-merge` mit zwei PMI-IDs) und rechnen die Anordnung im `GroupProcessor` selbst neu — rein translatorisch. Beim nächsten `setScene` wird die 90°-Drehung auf achsparallel zurückgeplättet.
**Warum schwer — das Dock-Modell ist translation-only, einachsig, gemeinsame Orientierung. Links/Rechts geht nur, weil nie rotiert wird.** Zu ändernde Schichten:
1. `Edge` = `{from,to,fromPmi,toPmi}` kennt keine Rotation → relative Rotation pro Kante nötig.
2. `computePositions` dockt mit `pos(to)=pos(from)+pmi(from)pmi(to)` (nur Position) → Rotation entlang der Kanten komponieren.
3. `pmiLocal` liest nur `position.value`. **Die Orientierung steckt schon in den Daten** (`properties.rotation`), wird aber ignoriert — und Format passt nicht: 3 Euler-Grad (z. B. `CS_EIN_90_0_0``[0,-0,180]`), deklariert als „quaternion", während `transformMath` `[w,x,y,z]` erwartet.
4. Placement-Mode hartverdrahtet auf `.name-CS_LINKS``.name-CS_RECHTS`; Steher-Anschlüsse leben in der `CS_EIN_*`-Familie → neue Drag-/Drop-Selektoren + Open-End-Logik (`occupiedPmis`).
5. `buildDetachEvent`/`areConnected` nehmen einen 1-achsigen Lauf an (dominante Achse, Links→Rechts-Leseordnung) → 90°-Anbau macht den Verbau zum 2D-Baum, Annahmen brechen.
**Datenlage:** kein plain `CS_VORNE`/`CS_HINTEN` (0 Teile); `CS_LINKS`/`CS_RECHTS` nur auf 12 Teilen; Vorne/Hinten nur in `CS_EIN_*` (rotationscodiert: `CS_EIN_VORNE`, `CS_EIN_HINTEN`, `CS_EIN_STEHER_VORNE/HINTEN`, `CS_EIN_90_0_0` …).
**Machbar:** Erweiterung unseres Modells (Rotation konsumieren + entlang Kanten propagieren + Format-Fix), keine Nachgenerierung der Teile — die nötige Orientierung liefern die Punktdaten bereits.
Siehe [[cpq-verbau-architektur]], [[fire-visual-placement-mode]].
+1
View File
@@ -28,3 +28,4 @@
- [Projekt: Bestätigen-Bug (FSA-Loop)](project_bestaetigen_bug.md) — Edge/Windows beim Kollegen; morgen Policy + Konsolen-reason prüfen
- [Projekt: Prod-Ist-Stand](project_prod_ist_stand.md) — Prod (ocp-01) LIVE: backend 0.38.4 Running, Secrets inkl. SAP gemountet; nur PR→main mergen offen; Quota 3CPU knapp
- [SAP-Config](sap-config.md) — echter Zugriff via Basic Auth (sy08804394), Key-Predicate-URL über Proxy; Env→SealedSecret/Overlay-Mapping im ops-Repo; Adapter-Patch offen
- [Projekt: Optimistische Sperre](project_optimistic_locking.md) — VERSION_CONFLICT (409) in persistCustomer gegen Last-Write-Wins; greift nur bei version!=null (Re-Send bleibt idempotent); lastUpdateTs ist server-autoritativ
@@ -0,0 +1,25 @@
---
name: project-optimistic-locking
description: "Server-seitige optimistische Sperre für Customer-Writes (VERSION_CONFLICT) — Fix gegen Last-Write-Wins bei zwei Sales am selben Kunden, eingebaut 2026-06-29."
metadata:
node_type: memory
type: project
originSessionId: 31351046-6fed-46e2-b117-bcc9d8064774
---
Vor Go-live (Donnerstag 2026-07-02) das Concurrency-Verhalten der Customer-Writes gefixt.
**Ausgangslage (charakterisiert):** `@VersionColumn` war im Schreibpfad wirkungslos — `persistCustomer` machte `repo.merge(existing, scalars)`, das die `version` aus dem Client-Payload übernahm. Folge: Last-Write-Wins, `version` lief sogar rückwärts (2→1), stiller Datenverlust bei parallelen Editoren. Jeder Sales hat prinzipiell alle Kunden des Landes → der Fall ist real.
**Fix (rein BE, kein FE-Eingriff):**
- `libs/shared/src/lib/errors/error-catalog.ts`: neuer Code `VERSION_CONFLICT` (HTTP 409).
- `libs/customer/customer-be/src/lib/customer-core.service.ts`: `enforceVersion()` in `persistCustomer` vor dem Merge — `incoming.version !== existing.version``DomainException('VERSION_CONFLICT', …, forcedState: existing)`. Danach zählt `@VersionColumn` wieder hoch.
- **Kernregel:** Sperre greift nur, wenn `incoming.version != null`. Ein nie synchronisierter Neuanlage-Stand (version undefined), der nach Crash erneut gesendet wird, ist ein idempotenter Re-Send — sonst wäre at-least-once / Crash-Recovery (S8b) kaputtgegangen. Ein bereits synchronisierter Datensatz trägt clientseitig immer eine version (reconcile schreibt den Server-Stand lokal zurück).
**Verhalten:** stale Edit → 409 → FE rollt über den vorhandenen `forcedState`-Mechanismus (wie SAP-Readonly) auf den Server-Stand zurück, Fehler sichtbar. Verliererseite verliert ihre Änderung (mit Meldung); ein Merge-Dialog wäre eine spätere fachliche Erweiterung.
**Wichtig für künftige Fixes:** `version` ist als „wer ist neuer"-Vergleichsbasis untauglich (client-steuerbar). `lastUpdateTs` ist server-autoritativ (`@BeforeUpdate``Date.now()`) — das ist die verlässliche Basis. Belegt in den Tests.
**Tests:** `customer-core.concurrency.spec.ts` (5, BE-direkt gegen In-Memory-SQLite) + `sync-loop.integration.spec.ts` S10 (End-to-End Multi-Sales). Gesamt 8 Projekte grün. Siehe [[project-sync-testing]] für den Test-Harness.
**Lokaler Testlauf:** node 22 via fnm nötig (`eval "$(/opt/homebrew/bin/fnm env)" && fnm use 22`), better-sqlite3 baut nicht gegen node 26. node_modules waren nicht installiert → `npm ci`. Repo: `~/projects/fahrzeugeinrichtung-plato-backend`.