Initial commit

This commit is contained in:
marcus.hinz
2026-07-09 09:22:00 +02:00
commit ed79bd00ce
30 changed files with 4014064 additions and 0 deletions
Vendored
BIN
View File
Binary file not shown.
+24
View File
@@ -0,0 +1,24 @@
{
"autoMemoryDirectory": "~/claude-projects/08-07/memory",
"hooks": {
"SessionStart": [
{
"hooks": [
{
"command": "jq -Rs '{systemMessage: ., hookSpecificOutput:{hookEventName:\"SessionStart\", additionalContext: .}}' /Users/marcus.hinz/claude-projects/08-07/OFFENE-PUNKTE.md",
"type": "command"
}
]
}
]
},
"permissions": {
"additionalDirectories": [
"~/projects/08-07"
],
"allow": [
"Edit(~/projects/08-07/**)",
"Edit(~/claude-projects/08-07/**)"
]
}
}
+1
View File
@@ -0,0 +1 @@
.ai-control-running
+3
View File
@@ -0,0 +1,3 @@
# Offene Punkte — bei jedem Start prüfen und abhaken
Keine offenen Punkte.
+8
View File
@@ -0,0 +1,8 @@
{
"pool": "private",
"terminal": {
"theme": "one-dark",
"icon": "08-07.png",
"title": "08-07"
}
}
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+294808
View File
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
- [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
- [Vorschlagsengine](project-08-07-vorschlagsengine.md) — Korrelationsanalyse (was wird im Bereich häufig gewählt), kein Filter+Kopie; Merkmale ohne Einfluss weglassen
- [Feedback: keine Datenmengen, kurz](feedback-keine-datenmengen-kurz.md) — nicht über Datenmenge reden, Prototyp, knapp bleiben
+14
View File
@@ -0,0 +1,14 @@
---
name: feedback-keine-datenmengen-kurz
description: Nicht über Datenmengen des Projekts reden; im Prototyp-Stadium; kurz fassen
metadata:
node_type: memory
type: feedback
originSessionId: 11f6b0de-5054-432e-9015-f2622d2236ce
---
Nicht ungefragt auf die Datenmenge (z. B. „191 Aufträge zu wenig für X") eingehen und keine daraus abgeleiteten Einschränkungen anhängen. Marcus ist im Prototyp-Stadium und kennt die tatsächliche Datenmenge selbst.
**Why:** Wiederholte Hinweise auf die Datenbasis wurden als Zutexten empfunden und ausdrücklich untersagt.
**How to apply:** Antworten knapp halten, Frage exakt beantworten, keine Caveats zur Datenmenge, keine Skalierungs-/Statistik-Vorbehalte ergänzen. Siehe [[project-08-07-datenbasis]].
+18
View File
@@ -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.
+22
View File
@@ -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-08-07-vorschlagsengine
description: "Vorschlagsengine arbeitet mit Korrelationsanalyse, nicht mit Profil-Filter + LLM-Kopie"
metadata:
node_type: memory
type: project
originSessionId: 3b989d4f-977f-430d-805a-0be687e3ac08
---
Die Vorschlagsengine (Tab „Vorschlag") soll zeigen, was im Bereich (Segment/Profil) **häufig gewählt** wird — Basis ist eine **Korrelationsanalyse** über den Bestand, kein hartes Filtern der Aufträge und Nachbauen einer Konfiguration.
**Why:** Marcus hat die erste Umsetzung (Profil-Filter → Kandidatenliste → LLM stellt Konfiguration zusammen) abgelehnt: „es geht nicht darum, Angebote zu filtern und zu kopieren".
**How to apply:** Je Modul messen, welche Profilmerkmale (Gewerk, Land, Größe, Fahrzeug) die Wahl statistisch beeinflussen (z. B. Lift/Häufigkeitsvergleich gegen Gesamtbestand). Merkmale ohne messbaren Einfluss fließen nicht ein und werden weggelassen. Das **LLM ist der Mehrwert und bleibt drin**: Die Statistik liefert die Korrelationsdaten, das LLM macht daraus den Vorschlag mit Begründung — Statistik ersetzt das LLM nicht. Verwandt: [[project-08-07-ai-assistent]], [[project-08-07-datenbasis]].
+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]].
+17
View File
@@ -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`.