feat: Introduce AI estimation and quote generation scripts, update pricing logic and PDF components, add new documentation, and clean up temporary files.

This commit is contained in:
2026-02-03 16:05:59 +01:00
parent 9751d2f61f
commit 788c7aa7df
46 changed files with 2314 additions and 2678 deletions

5
.gitignore vendored
View File

@@ -40,3 +40,8 @@ pnpm-debug.log*
# testing
/coverage
# local cache & data
.cache/
cloned-websites/
storage/

File diff suppressed because one or more lines are too long

246
data/briefings/etib.txt Normal file
View File

@@ -0,0 +1,246 @@
Hallo Marc,
eine harte Deadline gibt es nicht Was denkst du ist realistisch? Ich habe als Ziel so
April / Mai im Kopf -> dann aber schon zu 95 % fertig. Viele Grüße
Mit freundlichen Grüßen
Danny Joseph
Geschäftsführer
E-TIB GmbH
Gewerbestraße 22
D-03172 Guben
Mobil +49 15207230518
E-Mail d.joseph@e-tib.com
Web www.e-tib.com
--------------------------------------------------------------------------------------------------
Hey,
ich würde wie bei https://www.schleicher-gruppe.de/ ein Video auf der Startseite
haben wollen. Da ginge sicherlich was vom bisherigen Messevideo. Liebe Grüße.
Mit freundlichen Grüßen
Danny Joseph
Geschäftsführer
E-TIB GmbH
Gewerbestraße 22
D-03172 Guben
Mobil +49 15207230518
E-Mail d.joseph@e-tib.com
Web www.e-tib.com
--------------------------------------------------------------------------------------------------
Geschäftsführung: Danny Joseph
Handelsregister: Amtsgericht Cottbus
HRB: 12403 CB
USt. ID-Nr.: DE304799919
--------------------------------------------------------------------------------------------------
Von: Frieder Helmich <f.helmich@etib-ing.com>
Gesendet: Donnerstag, 29. Januar 2026 08:49
An: Marc Mintel <marc@cablecreations.de>; Danny Joseph <d.joseph@e-tib.com>
Betreff: AW: Homepage E-TIB
Hi Marc,
brauchst du nur Fotos oder bindest du auch videos ein? Wir haben sehr viel Videomaterial. Wir haben auch einen kleinen Film den wir auf der Messe laufen lassen haben.
Mit freundlichen Grüßen
i.A. Frieder Helmich
E-TIB Ingenieurgesellschaft mbH
Kampstraße 3
D-27412 Bülstedt
Tel +49 4283 6979923
Mobil +49 173 6560514
Fax +49 4283 6084091
E-Mail f.helmich@etib-ing.com
Web www.etib-ing.com
ETIB_Ing_logo_mk
Datenschutzhinweise: www.etib-ing.com/datenschutz
-----------------------------------------------------------------------------------------------
Geschäftsführung: Julian Helmich
Handelsregister: Amtsgericht Tostedt
HRB: 207158
-----------------------------------------------------------------------------------------------
Von: Marc Mintel <marc@cablecreations.de>
Gesendet: Mittwoch, 28. Januar 2026 18:10
An: Danny Joseph <d.joseph@e-tib.com>
Cc: Frieder Helmich <f.helmich@etib-ing.com>
Betreff: Re: Homepage E-TIB
Hallo Danny,
Vielen Dank für die schnelle Rückmeldung.
Wie gesprochen werde ich mir die Unterlagen und Webseiten im Detail anschauen und mich dann noch einmal bei dir melden.
Gibt es eigentlich eine Deadline oder einen zeitlichen Rahmen, wo ihr mit der neuen Webseite rechnen möchtet?
Je nach dem könnte man auch Features priorisieren, so dass der Kern der Seite schnellstmöglich modernisiert online geht und der Rest im Nachgang.
Das Foto-Material würde ich auch gerne sichten, dann kann man schon sehen, wie viel sich damit arbeiten lässt.
Viele Grüße
From: Danny Joseph <d.joseph@e-tib.com>
Organization: E-TIB GmbH
Date: Wednesday, 28. January 2026 at 16:16
To: Marc Mintel <marc@cablecreations.de>
Cc: 'Frieder Helmich' <f.helmich@etib-ing.com>
Subject: Homepage E-TIB
Hallo Marc,
wie telefonisch besprochen erste wirre Gedanken:
Wir möchten eine minimalistische, hochwertige Homepage die sowohl am PV, als auch
Auf Smartphone / Tablet etc. vernünftig ausschaut.
Bisher war unser Aufhänger:
DIE EXPERTEN FÜR KABELTIEFBAU …
Alles nur Ideen: …
# Schaltflächen ähnlich: https://www.schleicher-gruppe.de/
E-TIB GmbH
E-TIB Verwaltung GmbH
E-TIB Ingenieurgesellschaft mbH
E-TIB Bohrtechnik GmbH
# Schaltflächen ähnlich: https://www.schleicher-gruppe.de/
(ehemals Kompetenzen www.e-tib.com)
Kabelbau
Kabelpflugarbeiten
Horizontalspülbohrungen
Elektromontagen bis 110 kV
Glasfaser-Kabelmontagen
Wartung & Störungsdienst
Genehmigungs- und Ausführungsplanung
Komplexe Querung (Bahn, Autobahn, Gewässer)
Elektro- und Netzanschlussplanung
Vermessung & Dokumentation
Input für Über uns: Grid … Timeline?
Gründung E-TIB GmbH: 16.12.2015
Kabelbau
Kabelpflugarbeiten
Horizontalspülbohrungen
Elektromontagen bis 110 kV
Glasfaser-Kabelmontagen
Wartung & Störungsdienst
Elektro- und Netzanschlussplanung
Vermessung & Dokumentation
Gründung E-TIB Verwaltung GmbH: 14.11.2019
Der Erwerb, die Vermietung, Verpachtung und Verwaltung
von Immobilien, Grundstücken, Maschinen und Geräten.
Gründung E-TIB Ingenieurgesellschaft mbH: 04.02.2019
Genehmigungs- und Ausführungsplanung
Komplexe Querung (Bahn, Autobahn, Gewässer)
Elektro- und Netzanschlussplanung
Gründung E-TIB Bohrtechnik GmbH: 21.10.2025
Horizontalspülbohrungen in allen Bodenklassen
GruppenKacheln (Beispieltexte) ...
ETIB GmbH Ausführung elektrischer Infrastrukturprojekte
ETIB Bohrtechnik GmbH Präzise Horizontalbohrungen in allen Bodenklassen
ETIB Verwaltung GmbH Zentrale Dienste, Einkauf, Finanzen
ETIB Ingenieurgesellschaft mbH Planung, Projektierung, Dokumentation
Kontaktseite siehe: www.e-tib.com
Karriere: ...
Messen: wo wir dieses Jahr einen Stand haben: Intersolar München, Windenergietage Linstow, Kabelwerkstatt Wiesbaden
Referenzen: … müsste ich dir zur Verfügung stellen
Pflichtseiten
Impressum (vollständig, Verantwortliche, Registernummer, UStID).
Datenschutz (Verarbeitungen, Rechtsgrundlagen, AVV, CookieGruppen, Löschfristen, Rechte).
CookieEinstellungen (Consent Manager: ...)
www.e-tib.com
www.etib-ing.com
Hier mein instagram account:
me.and.eloise
Verstehst du mich vielleicht ein kleines Stück mehr…
Unser Frieder Helmich kann erstes Foto-/Videomaterial zur Verfügung stellen:
f.helmich@etib-ing.com
Lass mir mal eine Idee vom Stundenaufwand / Kosten pro Stunde für Erstellung zukommen,
damit wir eine Vertragsgrundlage haben. Danach lass uns loslegen.
Besten Dank dir.
Mit freundlichen Grüßen
Danny Joseph
Geschäftsführer
E-TIB GmbH
Gewerbestraße 22
D-03172 Guben
Mobil +49 15207230518
E-Mail d.joseph@e-tib.com
Web www.e-tib.com
--------------------------------------------------------------------------------------------------
Geschäftsführung: Danny Joseph
Handelsregister: Amtsgericht Cottbus
HRB: 12403 CB
USt. ID-Nr.: DE304799919
--------------------------------------------------------------------------------------------------
Von: Marc Mintel <marc@cablecreations.de>
Gesendet: Donnerstag, 13. November 2025 16:30
An: d.joseph@e-tib.com
Betreff: Homepage
Hi Danny,
mein Vater meinte, ich könnte mich mal bei dir melden, weil ihr jemanden für eure Website sucht.
Kurz zu mir: Ich habe über 10 Jahre in der Webentwicklung gearbeitet. Inzwischen liegt mein Schwerpunkt zwar im 3D-Bereich (u. a. cablecreations.de), aber ich betreue weiterhin Websites für Firmen, die das Ganze unkompliziert abgegeben haben möchten. Unter anderem betreue ich auch die Seite von KLZ (klz-cables.com). Der Ablauf ist bei mir recht einfach: Wenn ihr etwas braucht, reicht in der Regel eine kurze Mail Anpassungen, Inhalte oder technische Themen erledige ich dann im Hintergrund. Dadurch spart ihr euch Schulungen, Zugänge oder lange Meetings, wie man sie oft mit Agenturen hat.
Wichtig ist: Eine Website braucht auch nach dem Aufbau regelmäßige Pflege, damit Technik und Sicherheit sauber laufen das übernehme ich dann ebenfalls, damit ihr im Alltag keinen Aufwand damit habt.
Um einschätzen zu können, ob und wie ich euch unterstützen kann, wäre es gut zu wissen, was ihr mit der Website vorhabt und was an der aktuellen Seite nicht mehr passt. Wenn du magst, können wir dazu auch kurz telefonieren.
Viele Grüße
Marc
Marc Mintel
Founder & 3D Artist
marc@cablecreations.de
Cable Creations
www.cablecreations.de
info@cablecreations.de
VAT: DE367588065
Georg-Meistermann-Straße 7
54586 Schüller
Germany

73
docs/AUTOMATION.md Normal file
View File

@@ -0,0 +1,73 @@
# Routine Automation
*Kleine Helfer, die den Alltag deutlich entlasten*
In vielen mittelständischen Unternehmen fressen wiederkehrende Aufgaben Monat für Monat unzählige Stunden:
- Daten aus Dokumenten abtippen
- Formulare von Hand ausfüllen
- Angebote, Berichte oder Bestätigungen manuell anpassen
- Eingehende Anfragen immer wieder neu prüfen und bearbeiten
Das ist keine wertschöpfende Arbeit.
Das ist Routine, die teuer ist, Fehler produziert und gute Mitarbeiter davon abhält, sich um das zu kümmern, was wirklich Umsatz bringt.
Ich baue genau für diese Routine **einfache, maßgeschneiderte Helfer** meist mit PDF- oder Excel-Ausgabe, Konfiguratoren oder KI-Dokumenten-Einlesen.
Einmal eingerichtet, laufen sie leise im Hintergrund.
Kein großes Projekt. Kein monatliches Tool-Abo. Kein „lernen Sie das neue System“.
### Was das konkret bringen kann Beispiele aus der Praxis
- **Schnelle Dokumentenerstellung (PDF-Generatoren)**
Kurze Eingaben (Formular, Mail, Excel-Zeile) → fertiges PDF raus: Angebote, Berichte, Protokolle, Bestätigungen, Übersichten. Immer Ihr Corporate Design, immer aktuelle Daten/Bausteine.
→ Von 30120 Minuten runter auf 210 Minuten.
- **Excel-Automatisierungen & smarte Tabellen**
Verkaufszahlen, Lagerbestände, Kundenlisten → automatische Berechnungen, Zusammenfassungen, Prognosen oder Exporte. Monatsberichte oder Preislisten aktualisieren sich von selbst.
→ Kein ständiges Nachrechnen mehr, keine Versionskonflikte.
- **Konfiguratoren für Anfragen & Schätzungen**
Kunde oder Mitarbeiter geht schrittweise durch ein Formular (auf Ihrer Website oder intern): „Welche Leistung? Welcher Umfang? Welcher Termin?“ → sofort realistische Schätzung, Preisspanne oder fertiges Angebot als PDF/Excel.
- **KI-Einlesen von PDFs oder handschriftlichen Dokumenten**
Eingescannte Rechnungen, Lieferscheine, Formulare, Notizen oder handgeschriebene Protokolle → KI liest Text, Zahlen, Felder aus (auch Handschrift, wo lesbar) → Daten landen in übersichtlicher Tabelle/Excel oder vorausgefülltem Formular.
Mitarbeiter prüft nur noch kurz → kleine Korrektur → Prozess geht weiter.
→ Kein stundenlanges Abtippen mehr, deutlich schnellerer Durchlauf.
### Der echte Wert für Sie
- 3080 % weniger Zeit bei Routineaufgaben → Ihre Teams konzentrieren sich aufs Wesentliche
- Weniger Fehler & Rückfragen → einheitlicher, professioneller Output
- Schnellere Reaktion auf Kunden → Konfiguratoren & KI-Einlesen liefern sofort Infos
- Amortisation oft schon nach wenigen Wochen oder Dutzend Nutzungen
- Nutzt, was Sie bereits haben: Website, Excel, Mail, Scanner-App
### Was ich **nicht** mache
Ich ersetze **kein** ERP, CRM, Buchhaltungs- oder HR-System.
Kein automatisches Buchen, keine Finanzamtschnittstelle, keine GoBD-Archivierungspflichten.
Nur smarte Abkürzungen bei Routine der Rest bleibt in Ihren bewährten Tools.
### Ich kann Ihnen helfen, wenn Sie mit diesen typischen Problemen kämpfen
- „Wir tippen immer noch Daten aus gescannten Dokumenten oder handschriftlichen Notizen ab.“
- „Angebote, Berichte oder Protokolle dauern ewig, weil alles von Hand angepasst wird.“
- „Kunden fragen ständig dasselbe wir antworten jedes Mal manuell.“
- „Excel-Tabellen und Berechnungen werden ständig neu gemacht und gehen kaputt.“
- „Bis wir eine realistische Schätzung oder ein Angebot raus haben, vergeht zu viel Zeit.“
Schreiben Sie mir einfach einen kurzen Satz zu Ihrem größten Zeitfresser in diesem Bereich.
Ich antworte meist innerhalb von 12 Tagen:
- Ist das machbar? Ja/Nein
- Ca. wie viel Aufwand (meist 315 Stunden) & Preisrahmen
- Was Sie realistisch sparen können (Zeit, Nerven, Fehler)
Passt es → baue ich es.
Danach: Routine digitalisiert. Mehr Ruhe im Alltag.
**Kurz gesagt**
Routine Automation:
Nicht die große Revolution.
Sondern gezielte Entlastung bei den Dingen, die jeden Tag Zeit und Nerven kosten.
Mehr Zeit. Weniger Frust. Besserer Output.
Und das Gefühl: „Das läuft jetzt einfach.“
Wenn bei Ihnen gerade etwas „von Hand gemacht wird“ oder „ewig dauert“ Ich sage Ihnen, ob und wie schnell man das sinnvoll digitalisieren kann.

43
docs/PRINCIPLES.md Normal file
View File

@@ -0,0 +1,43 @@
Prinzipien
Ich arbeite nach klaren Grundsätzen, die sicherstellen, dass meine Kunden fair, transparent und langfristig profitieren.
1. Volle Preis-Transparenz
Alle Kosten sind offen und nachvollziehbar.
Es gibt keine versteckten Gebühren, keine Abos, keine Lock-ins.
Jeder Kunde sieht genau, wofür er bezahlt.
2. Quellcode & Projektzugang
Auf Wunsch erhalten Kunden jederzeit den vollständigen Source Code und eine nachvollziehbare Struktur.
Damit kann jeder andere Entwickler problemlos weiterarbeiten.
Niemand kann später behaupten, der Code sei „Messy“ oder unbrauchbar.
3. Best Practices & saubere Technik
Ich setze konsequent bewährte Standards und dokumentierte Abläufe ein.
Das sorgt dafür, dass Systeme wartbar, verständlich und erweiterbar bleiben langfristig.
4. Verantwortung & Fairness
Ich übernehme die technische Verantwortung für die Website.
Ich garantiere keine Umsätze, Rankings oder rechtliche Ergebnisse nur saubere Umsetzung und stabile Systeme.
Wenn etwas nicht sinnvoll ist, sage ich es ehrlich.
5. Langfristiger Wert
Eine Website ist ein Investment.
Ich baue sie so, dass Anpassungen, Erweiterungen und Übergaben an andere Entwickler problemlos möglich sind.
Das schützt Ihre Investition und vermeidet teure Neuaufbauten.
6. Zusammenarbeit ohne Tricks
Keine künstlichen Deadlines, kein unnötiger Overhead.
Kommunikation ist klar, Entscheidungen nachvollziehbar, Übergaben sauber dokumentiert.

98
docs/TECH.md Normal file
View File

@@ -0,0 +1,98 @@
Wie ich Websites technisch umsetze
Ich entwickle Websites als moderne, performante Websysteme nicht als Baukasten-Seiten und nicht als schwer wartbare CMS-Konstrukte.
Der Fokus liegt auf Geschwindigkeit, Stabilität, Datenschutz und langfristiger Wartbarkeit.
Die Technik dient dabei immer einem Zweck:
Ihre Website soll zuverlässig funktionieren, schnell laden und kein laufendes Risiko darstellen.
Geschwindigkeit & Performance
Meine Websites sind so aufgebaut, dass Inhalte extrem schnell ausgeliefert werden unabhängig davon, ob ein Besucher am Desktop oder mobil unterwegs ist.
Das bedeutet für Sie:
• kurze Ladezeiten
• bessere Nutzererfahrung
• messbar bessere Werte bei Google PageSpeed & Core Web Vitals
• geringere Absprungraten
Die Seiten werden nicht „zusammengeklickt“, sondern technisch optimiert ausgeliefert.
Responsives Design (ohne Kompromisse)
Jede Website ist von Grund auf responsiv.
Layout, Inhalte und Funktionen passen sich automatisch an:
• Smartphones
• Tablets
• Laptops
• große Bildschirme
Dabei wird nicht einfach skaliert, sondern gezielt für unterschiedliche Bildschirmgrößen optimiert.
Das Ergebnis ist eine saubere Darstellung und gute Bedienbarkeit auf allen Geräten.
Stabilität & Betriebssicherheit
Im Hintergrund laufen Überwachungs- und Kontrollmechanismen, die technische Probleme automatisch erkennen.
Für Sie heißt das:
• Fehler werden bemerkt, auch wenn niemand sie meldet
• ich werde aktiv informiert, statt erst zu reagieren, wenn etwas kaputt ist
• Probleme können frühzeitig behoben werden
Das reduziert Ausfälle und vermeidet unangenehme Überraschungen.
Datenschutz & DSGVO
Ich setze konsequent auf freie, selbst betriebene Software statt auf große externe Plattformen.
Ihre Vorteile:
• keine Weitergabe von Nutzerdaten an Dritte
• keine versteckten Tracker
• keine Abhängigkeit von US-Anbietern
• datenschutzfreundliche Statistik ohne Cookies
Die Website bleibt technisch schlank und rechtlich kontrollierbar.
Unabhängigkeit & Kostenkontrolle
Da ich keine proprietären Systeme oder Lizenzmodelle einsetze:
• entstehen keine laufenden Tool-Gebühren
• gibt es keine plötzlichen Preiserhöhungen
• bleibt die Website langfristig planbar betreibbar
Sie zahlen für die Leistung nicht für Lizenzen oder Marken.
Wartbarkeit & Erweiterbarkeit
Die technische Struktur ist so aufgebaut, dass:
• Inhalte erweitert werden können
• Funktionen sauber ergänzt werden können
• Anpassungen nicht das ganze System gefährden
Das schützt Ihre Investition und verhindert teure Neuaufbauten nach kurzer Zeit.
Kurz gesagt
Ich baue Websites, die:
• schnell sind
• auf allen Geräten sauber funktionieren
• datenschutzkonform betrieben werden
• technisch überwacht sind
• langfristig wartbar bleiben
Die Technik steht nicht im Vordergrund
aber sie sorgt dafür, dass Ihre Website zuverlässig ihren Zweck erfüllt.

42
docs/TONE.md Normal file
View File

@@ -0,0 +1,42 @@
Ton & Haltung in der Kommunikation
Dieses Dokument beschreibt die verbindlichen Prinzipien, nach denen ich mit Kunden kommuniziere schriftlich wie mündlich, auf der Website wie im Projektalltag.
1. Klarheit vor Höflichkeit
Ich kommuniziere klar, direkt und verständlich.
Unklare Formulierungen, Marketingfloskeln oder beschwichtigende Aussagen werden vermieden.
Lieber eine ehrliche, präzise Aussage als eine „freundliche“ Unverbindlichkeit.
2. Ehrlichkeit ohne Verkaufsdruck
Ich verspreche nichts, was ich nicht sicher einhalten kann.
Grenzen, Risiken und Unsicherheiten werden offen benannt.
Es gibt keine künstliche Dringlichkeit, kein Upselling aus Prinzip und keine verdeckten Interessen.
3. Sachlich, ruhig, professionell
Die Kommunikation bleibt sachlich und respektvoll auch bei Kritik, Verzögerungen oder Meinungsverschiedenheiten.
Emotionale Eskalation, Schuldzuweisungen oder Rechtfertigungsschleifen werden vermieden.
4. Verantwortung statt Ausreden
Probleme werden benannt, nicht relativiert.
Wenn etwas nicht funktioniert, wird erklärt warum und wie damit umgegangen wird.
Ich übernehme Verantwortung für meine Arbeit, nicht für äußere Faktoren außerhalb meines Einflusses.
5. Transparenz statt Fachchinesisch
Komplexe Sachverhalte werden verständlich erklärt, ohne künstliche Vereinfachung oder Herablassung.
Fachbegriffe werden nur verwendet, wenn sie notwendig sind.
Wissen dient der Orientierung des Kunden, nicht der Selbstdarstellung.
6. Gleichbehandlung aller Kunden
Alle Kunden werden gleich behandelt unabhängig von Projektgröße, Budget oder Laufzeit.
Es gibt keine versteckten Prioritäten, Sonderregeln oder impliziten Erwartungshaltungen.
7. Langfristige Perspektive
Die Kommunikation ist auf nachhaltige Zusammenarbeit ausgelegt, nicht auf kurzfristige Zustimmung.
Entscheidungen und Empfehlungen orientieren sich am langfristigen Nutzen des Kunden.

View File

@@ -1,64 +0,0 @@
import os
import re
showcase_dir = "public/showcase/klz-cables"
main_css = "wp-content/cache/breeze-minification/css/breeze_klz-cables-com-1-10895.css"
power_css = "wp-content/cache/breeze-minification/css/breeze_power-cables-1-43461.css"
fallback_img = "wp-content/uploads/2025/04/3.webp"
def fix_html_file(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
modified = False
# Fix CSS
links = re.findall(r'<link [^>]*rel=["\']stylesheet["\'][^>]*href=["\']([^"\']*)["\'][^>]*>', content)
links += re.findall(r'<link [^>]*href=["\']([^"\']*)["\'][^>]*rel=["\']stylesheet["\'][^>]*>', content)
for link in links:
file_dir = os.path.dirname(file_path)
rel_link = os.path.join(file_dir, link)
if not os.path.exists(rel_link):
fallback = main_css
if "power-cables" in file_path or "stromkabel" in file_path or "cables" in link:
fallback = power_css
levels = len(os.path.relpath(file_dir, showcase_dir).split(os.sep))
if os.path.relpath(file_dir, showcase_dir) == ".": levels = 0
new_link = ("../" * levels) + fallback
content = content.replace(link, new_link)
modified = True
# Fix Images (src)
imgs = re.findall(r'<img [^>]*src=["\']([^"\']*)["\'][^>]*>', content)
for img in imgs:
if img.startswith("data:"): continue
file_dir = os.path.dirname(file_path)
rel_img = os.path.join(file_dir, img)
if not os.path.exists(rel_img):
levels = len(os.path.relpath(file_dir, showcase_dir).split(os.sep))
if os.path.relpath(file_dir, showcase_dir) == ".": levels = 0
new_img = ("../" * levels) + fallback_img
content = content.replace(img, new_img)
modified = True
# Fix srcset (just remove or replace with single fallback)
srcsets = re.findall(r'srcset=["\']([^"\']*)["\']', content)
for srcset in srcsets:
# If any part of srcset is likely broken, just replace the whole thing with fallback
# Or simpler: if it contains "../wp-content", it's likely broken in this context
if "../wp-content" in srcset:
file_dir = os.path.dirname(file_path)
levels = len(os.path.relpath(file_dir, showcase_dir).split(os.sep))
if os.path.relpath(file_dir, showcase_dir) == ".": levels = 0
new_img = ("../" * levels) + fallback_img
content = content.replace(f'srcset="{srcset}"', f'srcset="{new_img} 800w"')
modified = True
if modified:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
for root, dirs, files in os.walk(showcase_dir):
for file in files:
if file.endswith(".html"):
fix_html_file(os.path.join(root, file))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -15,6 +15,7 @@
"clone-website": "tsx ./scripts/clone-recursive.ts",
"clone-page": "tsx ./scripts/clone-page.ts",
"generate-quote": "tsx ./scripts/generate-quote.ts",
"ai-estimate": "tsx ./scripts/ai-estimate.ts",
"video:preview": "remotion preview video/index.ts",
"video:render": "remotion render video/index.ts ButtonShowcase out/button-showcase.mp4",
"video:render:contact": "remotion render video/index.ts ContactFormShowcase out/contact-showcase.mp4 --concurrency=1 --codec=h264 --crf=16 --pixel-format=yuv420p --overwrite",

537
scripts/ai-estimate.ts Normal file
View File

@@ -0,0 +1,537 @@
import { CheerioCrawler, RequestQueue } from 'crawlee';
import * as path from 'node:path';
import * as fs from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { URL } from 'node:url';
import { execSync } from 'node:child_process';
import axios from 'axios';
import { FileCacheAdapter } from '../src/utils/cache/file-adapter.js';
import { initialState } from '../src/logic/pricing/constants.js';
async function main() {
const OPENROUTER_KEY = process.env.OPENROUTER_KEY;
if (!OPENROUTER_KEY) {
console.error('❌ Error: OPENROUTER_KEY not found in environment.');
process.exit(1);
}
let briefing = '';
let targetUrl: string | null = null;
let comments: string | null = null;
let cacheKey: string | null = null;
let jsonStatePath: string | null = null;
const args = process.argv.slice(2);
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--url') {
targetUrl = args[++i];
} else if (arg === '--comments' || arg === '--notes') {
comments = args[++i];
} else if (arg === '--cache-key') {
cacheKey = args[++i];
} else if (arg === '--json') {
jsonStatePath = args[++i];
} else if (!arg.startsWith('--')) {
briefing = arg;
}
}
if (briefing && briefing.startsWith('@')) {
const rawPath = briefing.substring(1);
const filePath = rawPath.startsWith('/') ? rawPath : path.resolve(process.cwd(), rawPath);
briefing = await fs.readFile(filePath, 'utf8');
}
// Discovery ONLY if not provided
if (!targetUrl && briefing) {
const urlMatch = briefing.match(/https?:\/\/[^\s]+/);
if (urlMatch) {
targetUrl = urlMatch[0];
console.log(`🔗 Discovered URL in briefing: ${targetUrl}`);
}
}
if (!briefing && !targetUrl && !comments && !jsonStatePath) {
console.error('❌ Usage: npm run ai-estimate -- "Briefing text" [--url https://example.com] [--comments "Manual notes"]');
console.error(' Or: npm run ai-estimate -- @briefing.txt [--url https://example.com]');
console.error(' Or: npm run ai-estimate -- --json path/to/state.json');
process.exit(1);
}
const clearCache = process.argv.includes('--clear-cache');
if (clearCache) {
console.log('🧹 Clearing cache...');
const cacheFiles = await fs.readdir(path.join(process.cwd(), '.cache'));
for (const file of cacheFiles) {
if (file.startsWith('ai_est_')) {
await fs.unlink(path.join(process.cwd(), '.cache', file));
}
}
}
const cache = new FileCacheAdapter({ prefix: 'ai_est_' });
const finalCacheKey = cacheKey || `${briefing}_${targetUrl}_${comments}`;
// 1. Crawl if URL provided
let crawlContext = '';
if (targetUrl) {
console.log(`🔍 Crawling ${targetUrl} for context...`);
const cachedCrawl = await cache.get<string>(`crawl_${targetUrl}`);
if (cachedCrawl && !clearCache) {
console.log('📦 Using cached crawl results.');
crawlContext = cachedCrawl;
} else {
crawlContext = await performCrawl(targetUrl);
await cache.set(`crawl_${targetUrl}`, crawlContext, 86400); // 24h cache
}
}
// 2. AI Prompting
console.log('🤖 Consultating Gemini 3 Flash...');
const cachedAi = !clearCache ? await cache.get<any>(finalCacheKey) : null;
let formState: any;
let usage: { prompt: number, completion: number, cost: number } = { prompt: 0, completion: 0, cost: 0 };
// Load Context Documents
const principles = await fs.readFile(path.resolve(process.cwd(), 'docs/PRINCIPLES.md'), 'utf8');
const techStandards = await fs.readFile(path.resolve(process.cwd(), 'docs/TECH.md'), 'utf8');
const tone = await fs.readFile(path.resolve(process.cwd(), 'docs/TONE.md'), 'utf8');
if (jsonStatePath) {
console.log(`📂 Loading state from JSON: ${jsonStatePath}`);
const rawJson = await fs.readFile(path.resolve(process.cwd(), jsonStatePath), 'utf8');
formState = JSON.parse(rawJson);
} else if (cachedAi) {
console.log('📦 Using cached AI response.');
formState = cachedAi;
} else {
const result = await getAiEstimation(briefing, crawlContext, comments, OPENROUTER_KEY, principles, techStandards, tone);
formState = result.state;
usage = result.usage;
await cache.set(finalCacheKey, formState);
}
// 3. Save Data & Generate PDF
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const jsonOutDir = path.resolve(process.cwd(), 'out/estimations/json');
if (!existsSync(jsonOutDir)) await fs.mkdir(jsonOutDir, { recursive: true });
const finalJsonPath = path.join(jsonOutDir, `${formState.companyName || 'unknown'}_${timestamp}.json`);
await fs.writeFile(finalJsonPath, JSON.stringify(formState, null, 2));
const tempJsonPath = path.resolve(process.cwd(), '.cache', `temp_state_${Date.now()}.json`);
await fs.writeFile(tempJsonPath, JSON.stringify(formState, null, 2));
console.log(`📦 Saved detailed state to: ${finalJsonPath}`);
console.log('📄 Generating PDF estimation...');
try {
execSync(`npx tsx ./scripts/generate-quote.ts --input ${tempJsonPath}`, { stdio: 'inherit' });
} finally {
// await fs.unlink(tempJsonPath);
}
console.log('\n✨ AI Estimation Complete!');
if (usage.prompt > 0) {
console.log('--------------------------------------------------');
console.log('📊 ACCUMULATED API USAGE (SUM OF 6 PASSES)');
console.log(` Model: google/gemini-3-flash-preview`);
console.log(` Total Prompt: ${usage.prompt.toLocaleString()}`);
console.log(` Total Completion: ${usage.completion.toLocaleString()}`);
console.log(` Total Tokens: ${(usage.prompt + usage.completion).toLocaleString()}`);
console.log(` Total Cost (USD): $${usage.cost.toFixed(6)}`);
console.log('--------------------------------------------------\n');
}
}
async function performCrawl(url: string): Promise<string> {
const pages: { url: string, content: string, type: string }[] = [];
const origin = new URL(url).origin;
const crawler = new CheerioCrawler({
maxRequestsPerCrawl: 20,
async requestHandler({ $, request, enqueueLinks }) {
const title = $('title').text();
const urlObj = new URL(request.url);
const urlPath = urlObj.pathname.toLowerCase();
let type = 'other';
if (urlPath === '/' || urlPath === '') type = 'home';
else if (urlPath.includes('service') || urlPath.includes('leistung')) type = 'service';
else if (urlPath.includes('blog') || urlPath.includes('news') || urlPath.includes('aktuelles') || urlPath.includes('magazin')) type = 'blog';
else if (urlPath.includes('contact') || urlPath.includes('kontakt')) type = 'contact';
else if (urlPath.includes('job') || urlPath.includes('karriere') || urlPath.includes('career') || urlPath.includes('human-resources')) type = 'career';
else if (urlPath.includes('portfolio') || urlPath.includes('referenz') || urlPath.includes('projekt') || urlPath.includes('case-study')) type = 'portfolio';
else if (urlPath.includes('legal') || urlPath.includes('impressum') || urlPath.includes('datenschutz') || urlPath.includes('privacy')) type = 'legal';
const h1s = $('h1').map((_, el) => $(el).text()).get();
const navLinks = $('nav a').map((_, el) => $(el).text().trim()).get().filter(t => t.length > 0);
const bodyText = $('body').text().replace(/\s+/g, ' ').substring(0, 50000);
const html = $.html();
const hexColors = html.match(/#(?:[0-9a-fA-F]{3}){1,2}\b/g) || [];
const uniqueColors = Array.from(new Set(hexColors)).slice(0, 5);
pages.push({
url: request.url,
type,
content: `Title: ${title}\nType: ${type}\nHeadings: ${h1s.join(', ')}\nNav: ${navLinks.join(', ')}\nColors: ${uniqueColors.join(', ')}\nText: ${bodyText}`
});
await enqueueLinks({
limit: 15,
transformRequestFunction: (req) => {
const reqUrl = new URL(req.url);
if (reqUrl.origin !== origin) return false;
// Skip assets
if (reqUrl.pathname.match(/\.(pdf|zip|jpg|png|svg|webp)$/i)) return false;
return req;
}
});
},
});
await crawler.run([url]);
const typeCounts = pages.reduce((acc, p) => {
acc[p.type] = (acc[p.type] || 0) + 1;
return acc;
}, {} as Record<string, number>);
let summary = `\nCrawl Summary: Identified ${pages.length} pages total on ${origin}.\n`;
summary += Object.entries(typeCounts).map(([type, count]) => `- ${type}: ${count}`).join('\n') + '\n\n';
return summary + pages.map(p => `--- PAGE: ${p.url} ---\n${p.content}`).join('\n\n');
}
async function getAiEstimation(briefing: string, crawlContext: string, comments: string | null, apiKey: string, principles: string, techStandards: string, tone: string) {
let usage = { prompt: 0, completion: 0, cost: 0 };
const addUsage = (data: any) => {
if (data?.usage) {
usage.prompt += data.usage.prompt_tokens || 0;
usage.completion += data.usage.completion_tokens || 0;
// OpenRouter provides 'cost' field in USD (as per documentation)
// If missing, we use a fallback calculation for transparency
if (data.usage.cost !== undefined) {
usage.cost += data.usage.cost;
} else {
// Fallback: Gemini 3 Flash Flash pricing (~$0.1 / 1M prompt, ~$0.4 / 1M completion)
usage.cost += (data.usage.prompt_tokens || 0) * (0.1 / 1000000) + (data.usage.completion_tokens || 0) * (0.4 / 1000000);
}
}
};
// 1. PASS 1: Fact Extraction
console.log(' ↳ Pass 1: Fact Extraction...');
const pass1SystemPrompt = `
You are a precision extraction engine. Analyze the briefing and extract ONLY the raw facts.
Tone: Literal, non-interpretive.
Output language: GERMAN (Strict).
### OBJECTIVES:
- Extract companyName (Strictly the name, no descriptors).
- Extract companyAddress (Full address if found).
- Extract personName (Primary contact if found).
- Extract **websiteTopic**: This MUST be a single, short branch name (e.g., "Kabeltiefbau", "Logistik", "Anwaltskanzlei"). ABSOLUTELY NO SENTENCES. If the briefing says "Group-Homepage for X", extract ONLY "X".
- Map to internal IDs for selectedPages, features, functions, apiSystems, assets.
- Identify if isRelaunch is true (briefing mentions existing site or URL).
- For all textual values (deadline, websiteTopic, targetAudience etc.): USE GERMAN.
- **multilang**: ONLY if the briefing mentions multiple target languages (e.g., DE/EN). If only one language is mentioned, do NOT use multilang.
- **maps**: If "Google Maps" or location maps are mentioned or implicit (Contact page).
- **CRITICAL**: Do NOT include "social" in apiSystems unless the user explicitly wants to SYNC/POST content to social media. "Existing social media links" are NOT apiSystems.
- **CRITICAL**: "Video Player", "Cookie Banner", "Animations" are NOT features. They are visual/base. Do NOT map them to features.
### CATEGORY MAPPING (IDs ONLY):
- **selectedPages**: [Home, About, Services, Contact, Landing, Legal]
- **features**: [blog_news, products, jobs, refs, events]
- **functions**: [search, filter, pdf, forms, members, calendar, multilang, chat]
- **apiSystems**: [crm_erp, payment, marketing, ecommerce, maps, social, analytics]
- **assets**: [existing_website, logo, styleguide, content_concept, media, icons, illustrations, fonts]
### OUTPUT FORMAT (Strict JSON):
{
"companyName": string,
"companyAddress": string,
"personName": string,
"websiteTopic": string,
"isRelaunch": boolean,
"selectedPages": string[],
"features": string[],
"functions": string[],
"apiSystems": string[],
"assets": string[],
"deadline": string (GERMAN),
"targetAudience": "B2B" | "B2C" | "Internal" | string (GERMAN),
"expectedAdjustments": "low" | "medium" | "high" | string (GERMAN),
"employeeCount": string
}
`;
const pass1UserPrompt = `BRIEFING:\n${briefing}\n\nCOMMENTS:\n${comments}\n\nCRAWL:\n${crawlContext}`;
const p1Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview',
messages: [{ role: 'system', content: pass1SystemPrompt }, { role: 'user', content: pass1UserPrompt }],
response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
addUsage(p1Resp.data);
const facts = JSON.parse(p1Resp.data.choices[0].message.content);
// 2. PASS 2: Feature Deep-Dive
console.log(' ↳ Pass 2: Feature Deep-Dive...');
const pass2SystemPrompt = `
You are a detail-oriented Solution Architect.
For EVERY item selected in Pass 1 (pages, features, functions, apiSystems), write a specific justification and technical scope.
### RULES:
1. **CONCRETE & SPECIFIC**: Do NOT say "Implementation of X". Say "Displaying X with Y filters".
2. **NO EFFECTS**: Do not mention "fade-ins", "animations" or "visual styling". Focus on FUNCTION.
3. **ABSOLUTE RULE**: EVERYTHING MUST BE GERMAN.
4. **TRANSPARENCY**: Explain exactly what the USER gets.
5. **API NOTE**: For 'media' or 'video', explicitly state "Upload & Integration" (NO STREAMING).
### INPUT (from Pass 1):
${JSON.stringify(facts, null, 2)}
### OUTPUT FORMAT (Strict JSON):
{
"pageDetails": { "Home": string, ... },
"featureDetails": { "blog_news": string, ... },
"functionDetails": { "search": string, ... },
"apiDetails": { "crm_erp": string, ... }
}
`;
const p2Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview',
messages: [{ role: 'system', content: pass2SystemPrompt }, { role: 'user', content: briefing }],
response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
addUsage(p2Resp.data);
const details = JSON.parse(p2Resp.data.choices[0].message.content);
// 3. PASS 3: Strategic Content
console.log(' ↳ Pass 3: Strategic Content...');
const pass3SystemPrompt = `
You are a high-end Digital Architect. Analyze the BRIEFING.
ABSOLUTE RULE: OUTPUT MUST BE 100% GERMAN.
### TONE & COMMUNICATION PRINCIPLES (MANDATORY):
${tone}
### OBJECTIVE:
1. **briefingSummary**: Summarize the project's essence for the CUSTOMER.
- FOLLOW PRINCIPLE 1 & 5: Clear, direct, no marketing fluff, no "partnership talk".
- Focus purely on the CUSTOMER'S goal: What are they building, why does it matter to their business, and what is the outcome?
- Keep it 2-3 professional, direct sentences.
2. **designVision**: A solid, grounded, and high-quality description of the look & feel.
- FOLLOW PRINCIPLE 1 & 3: Fact-based, professional, high density of information.
- **NO ARROGANCE**: Eliminate all "high-end", "world-class", "dominance" language. Be humble and precise.
- **SIMPLE & CLEAR**: Use simple German. No buzzwords. "Solid Industrial Design" instead of "Technocratic Sovereignty".
- 3-4 sentences of deep analysis.
### OUTPUT FORMAT (Strict JSON):
{
"briefingSummary": string,
"designVision": string
}
`;
const p3Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview',
messages: [{ role: 'system', content: pass3SystemPrompt }, { role: 'user', content: briefing }],
response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
addUsage(p3Resp.data);
const strategy = JSON.parse(p3Resp.data.choices[0].message.content);
// 4. PASS 4: Information Architecture (Sitemap)
console.log(' ↳ Pass 4: Information Architecture...');
const pass4SystemPrompt = `
You are a Senior UX Architect. Design a hierarchical sitemap following the 'Industrial Logic' principle.
EVERYTHING MUST BE IN GERMAN.
### SITEMAP RULES:
1. **HIERARCHY**: Build a logical tree. Group by category (e.g., "Kern-Präsenz", "Lösungen", "Vertrauen", "Rechtliches").
2. **INTENT**: Each page MUST have a title and a brief functional conversion intent (desc).
3. **COMPREHENSIVENESS**: Ensure all 'selectedPages' and 'features' from Pass 1 are represented.
4. **LANGUAGE**: STRICT GERMAN TITLES. Do NOT use "Home", "About", "Services". Use "Startseite", "Über uns", "Leistungen".
### DATA CONTEXT:
${JSON.stringify({ facts, strategy }, null, 2)}
### OUTPUT FORMAT (Strict JSON):
{
"websiteTopic": string,
"sitemap": [ { "category": string, "pages": [ { "title": string, "desc": string } ] } ]
}
`;
const p4Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview',
messages: [{ role: 'system', content: pass4SystemPrompt }, { role: 'user', content: briefing }],
response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
addUsage(p4Resp.data);
const ia = JSON.parse(p4Resp.data.choices[0].message.content);
// 5. PASS 5: Position Synthesis & Pricing Transparency
console.log(' ↳ Pass 5: Position Synthesis...');
const pass5SystemPrompt = `
You are a Senior Solution Architect. Your goal is ABSOLUTE TRANSPARENCY for the customer.
Each position in the quote must be perfectly justified and detailed.
### POSITION TITLES:
"Basis Website Setup", "Individuelle Seiten", "System-Module", "Logik-Funktionen", "Schnittstellen (API)", "Inhaltsverwaltung (CMS)".
### MAPPING RULES (STRICTLY BASED ON PRICING.MD):
- **Basis Website Setup**: Includes Infrastructure, Hosting Setup, Basic SEO, Cookie-Consent (!), Design-Template.
- **System-Module (Features)**: ONLY closed data systems: [Blog, News, Products, Jobs, Cases/References, Events].
- NEVER put "Video Player", "Cookies", "Animations" here.
- **Logik-Funktionen**: Functional logic like: Search, Filter, Forms, PDF-Export, Multi-lang.
- **Schnittstellen (API)**: REAL Data Syncs (CRM, ERP). DO NOT include Tracking, Google Maps, or simple Video embedding here. Basic embedding is "Basis Website Setup".
- **Sorglos-Betrieb (Hosting)**: Hosting & Maintenance.
### RULES FOR positionDescriptions:
1. **ZERO GENERALIZATION**: Do NOT say "Verschiedene Funktionen".
2. **ITEMIZED SYNTHESIS**: Mention EVERY component selected in Pass 1.
3. **BREVITY & DENSITY**: Max 1-2 short sentences. Focus on TASKS not RESULTS.
4. **STYLE**: Direct, engineering-grade, no fluff.
5. **LANGUAGE**: 100% GERMAN.
6. **SPECIFIC - PAGES**: For "Individuelle Seiten", list the pages as a comma-separated list (e.g. "Umfasst: Startseite, Über uns, Leistungen, Kontakt, Impressum").
7. **SPECIFIC - API**: Video Uploads, Google Maps, and Tracking are NOT APIs. If video/maps are standard embedding, do NOT put them in "Schnittstellen".
8. **SPECIFIC - HOSTING**: Always append: "Inkl. 20GB Speicher. Auto-Erweiterung +10€/10GB."
9. **SPECIFIC - LOGIC**: Describe the ACTUAL logic.
- BAD: "Erweiterte Formulare", "Logikfunktionen"
- GOOD: "Anfrage-Strecken mit Validierung", "Filterung nach Kategorie", "Mehrsprachigkeits-Routing".
### FORBIDDEN PHRASES (STRICT BLOCKLIST):
- "Erweiterte Formulare" (INSTEAD USE: "Komplexe Anfrage-Logik" or "Valide Formular-Systeme")
- "Verschiedene Funktionen"
- "Allgemeine Logik"
- "Optimierte Darstellung"
10. **NO "MARKETING LINGO"**: Never say "avoids branding" or "maximizes performance". Say "Implements HTML5 Video Player". ALWAYS DESCRIBE THE TASK.
### DATA CONTEXT:
${JSON.stringify({ facts, details, strategy, ia }, null, 2)}
### OUTPUT FORMAT (Strict JSON):
{
"positionDescriptions": { "Basis Website Setup": string, ... }
}
`;
const p5Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview',
messages: [{ role: 'system', content: pass5SystemPrompt }, { role: 'user', content: briefing }],
response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
addUsage(p5Resp.data);
const positionsData = JSON.parse(p5Resp.data.choices[0].message.content);
// 6. PASS 6: Reflection & Hardening
console.log(' ↳ Pass 6: Reflection & Nuance Check...');
const pass6SystemPrompt = `
You are a senior supervisor. Compare the CURRENT_STATE against the RAW_BRIEFING.
Your goal is to catch missed nuances, specific customer wishes, and technical details.
### CHECKLIST:
1. **SPECIFICS**: Did we miss names, technical terms (kV, HDD, etc.), or specific vendor refs?
2. **CONSISTENCY**: Do the positionDescriptions match the counts of features/functions in Pass 1?
3. **DEADLINE**: Is there a specific month? (e.g. April/Mai). If yes, set "deadline" field.
4. **LANGUAGE**: ABSOLUTE RULE: EVERYTHING MUST BE GERMAN.
5. **CONFLICT CHECK**: If 'languagesList' has only 1 item, REMOVE 'multilang' from 'functions'.
6. Refactor 'dontKnows' into a 'gridDontKnows' object for missing technical facts.
### CURRENT_STATE:
${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)}
`;
const p6Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview',
messages: [{ role: 'system', content: pass6SystemPrompt }, { role: 'user', content: `RAW_BRIEFING:\n${briefing}\n\nEnhance the state. Return ONLY the delta or the corrected fields.` }],
response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
addUsage(p6Resp.data);
const reflection = JSON.parse(p6Resp.data.choices[0].message.content);
let finalState = {
...initialState,
...facts,
...strategy,
...ia,
...positionsData,
...reflection,
statusQuo: facts.isRelaunch ? 'Relaunch' : 'Neuentwicklung'
};
// Flatten if AI nested everything under "0", "state" or "state.0"
if (finalState["0"]) finalState = { ...finalState, ...finalState["0"] };
if ((finalState as any).state) {
const nestedState = (finalState as any).state;
finalState = { ...finalState, ...nestedState };
if (nestedState["0"]) finalState = { ...finalState, ...nestedState["0"] };
}
// Normalization Layer: Map hallucinated German keys back to internal keys
const normalizationMap: Record<string, string> = {
"Briefing-Zusammenfassung": "briefingSummary",
"Design-Vision": "designVision",
"Zusammenfassung": "briefingSummary",
"Vision": "designVision",
"BRIEFING_SUMMARY": "briefingSummary",
"DESIGN_VISION": "designVision"
};
Object.entries(normalizationMap).forEach(([gerKey, intKey]) => {
if (finalState[gerKey] && !finalState[intKey]) {
if (typeof finalState[gerKey] === 'object' && !Array.isArray(finalState[gerKey])) {
finalState[intKey] = Object.values(finalState[gerKey]).join('\n\n');
} else {
finalState[intKey] = finalState[gerKey];
}
}
});
// Sitemap Normalization (German keys to internal)
if (Array.isArray(finalState.sitemap)) {
finalState.sitemap = finalState.sitemap.map((cat: any) => ({
category: cat.category || cat.kategorie || cat.Kategorie || cat.title || "Allgemein",
pages: (cat.pages || cat.seiten || cat.Seiten || []).map((page: any) => ({
title: page.title || page.titel || page.Titel || "Seite",
desc: page.desc || page.beschreibung || page.Beschreibung || page.description || ""
}))
}));
}
// Position Descriptions Normalization
if (finalState.positionDescriptions) {
const normalized: Record<string, string> = {};
Object.entries(finalState.positionDescriptions).forEach(([key, value]) => {
const normalizedKey = key === 'titel' || key === 'Title' ? 'title' : key;
const normalizedValue = typeof value === 'object' ? (value as any).beschreibung || (value as any).description || JSON.stringify(value) : value;
normalized[normalizedKey] = normalizedValue as string;
});
finalState.positionDescriptions = normalized;
}
// Normalize final state
if (Array.isArray(finalState.positionDescriptions)) {
const normalized: Record<string, string> = {};
finalState.positionDescriptions.forEach((item: any) => {
const key = item.feature || item.id || item.title || item.pos;
if (key) normalized[key] = item.description || item.desc;
});
finalState.positionDescriptions = normalized;
}
if (finalState.sitemap && !Array.isArray(finalState.sitemap)) {
if (finalState.sitemap.categories) finalState.sitemap = finalState.sitemap.categories;
else if (finalState.sitemap.sitemap) finalState.sitemap = finalState.sitemap.sitemap;
else {
const entries = Object.entries(finalState.sitemap);
if (entries.every(([_, v]) => Array.isArray(v))) {
finalState.sitemap = entries.map(([category, pages]) => ({ category, pages }));
}
}
}
return { state: finalState, usage };
}
main().catch(console.error);

154
scripts/generate-quote.ts Normal file
View File

@@ -0,0 +1,154 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as readline from 'node:readline/promises';
import { fileURLToPath } from 'node:url';
import { createElement } from 'react';
import { renderToFile } from '@react-pdf/renderer';
import { calculatePositions } from '../src/logic/pricing/calculator.js';
import { CombinedQuotePDF } from '../src/components/CombinedQuotePDF.js';
import { initialState, PRICING } from '../src/logic/pricing/constants.js';
import { getTechDetails, getPrinciples } from '../src/logic/content-provider.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
async function main() {
const args = process.argv.slice(2);
const isInteractive = args.includes('--interactive') || args.includes('-I');
const inputPath = args.find((_, i) => args[i - 1] === '--input' || args[i - 1] === '-i');
const outputPath = args.find((_, i) => args[i - 1] === '--output' || args[i - 1] === '-o');
let state = { ...initialState };
if (inputPath) {
const rawData = fs.readFileSync(path.resolve(process.cwd(), inputPath), 'utf8');
const diskState = JSON.parse(rawData);
state = { ...state, ...diskState };
}
if (isInteractive) {
state = await runWizard(state);
}
// Final confirmation of data needed for PDF
if (!state.name || !state.email) {
console.warn('⚠️ Missing recipient name or email. Document might look incomplete.');
}
const totalPrice = calculateTotal(state);
const monthlyPrice = calculateMonthly(state);
const totalPagesCount = (state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0);
const finalOutputPath = outputPath || generateDefaultPath(state);
const outputDir = path.dirname(finalOutputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Resolve assets for the PDF
const assetsDir = path.resolve(process.cwd(), 'src/assets');
const headerIcon = path.join(assetsDir, 'logo/Icon White Transparent.png');
const footerLogo = path.join(assetsDir, 'logo/Logo Black Transparent.png');
console.log(`🚀 Generating PDF: ${finalOutputPath}`);
const estimationProps = {
state,
totalPrice,
monthlyPrice,
totalPagesCount,
pricing: PRICING,
headerIcon,
footerLogo
};
await renderToFile(
createElement(CombinedQuotePDF as any, {
estimationProps,
techDetails: getTechDetails(),
principles: getPrinciples()
}) as any,
finalOutputPath
);
console.log('✅ Done!');
}
async function runWizard(state: any) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log('\n--- Mintel Quote Generator Wizard ---\n');
const ask = async (q: string, def?: string) => {
const answer = await rl.question(`${q}${def ? ` [${def}]` : ''}: `);
return answer || def || '';
};
const selectOne = async (q: string, options: { id: string, label: string }[]) => {
console.log(`\n${q}:`);
options.forEach((opt, i) => console.log(`${i + 1}) ${opt.label}`));
const answer = await rl.question('Selection (number): ');
const idx = parseInt(answer) - 1;
return options[idx]?.id || options[0].id;
};
state.name = await ask('Recipient Name', state.name);
state.email = await ask('Recipient Email', state.email);
state.companyName = await ask('Company Name', state.companyName);
state.projectType = await selectOne('Project Type', [
{ id: 'website', label: 'Website' },
{ id: 'web-app', label: 'Web App' }
]);
if (state.projectType === 'website') {
state.websiteTopic = await ask('Website Topic', state.websiteTopic);
// Simplified for now, in a real tool we'd loop through all options
}
rl.close();
return state;
}
function calculateTotal(state: any) {
// Basic duplication of logic from ContactForm for consistency
if (state.projectType !== 'website') return 0;
const totalPagesCount = (state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0);
let total = PRICING.BASE_WEBSITE;
total += totalPagesCount * PRICING.PAGE;
total += ((state.features?.length || 0) + (state.otherFeatures?.length || 0) + (state.otherFeaturesCount || 0)) * PRICING.FEATURE;
total += ((state.functions?.length || 0) + (state.otherFunctions?.length || 0) + (state.otherFunctionsCount || 0)) * PRICING.FUNCTION;
total += ((state.apiSystems?.length || 0) + (state.otherTech?.length || 0) + (state.otherTechCount || 0)) * PRICING.API_INTEGRATION;
if (state.cmsSetup) {
total += PRICING.CMS_SETUP;
total += ((state.features?.length || 0) + (state.otherFeatures?.length || 0) + (state.otherFeaturesCount || 0)) * PRICING.CMS_CONNECTION_PER_FEATURE;
}
const languagesCount = state.languagesList?.length || 1;
if (languagesCount > 1) {
total *= (1 + (languagesCount - 1) * 0.2);
}
return Math.round(total);
}
function calculateMonthly(state: any) {
if (state.projectType !== 'website') return 0;
return PRICING.HOSTING_MONTHLY + ((state.storageExpansion || 0) * PRICING.STORAGE_EXPANSION_MONTHLY);
}
function generateDefaultPath(state: any) {
const now = new Date();
const month = now.toISOString().slice(0, 7);
const day = now.toISOString().slice(0, 10);
const company = (state.companyName || state.name || 'Unknown').replace(/[^a-z0-9]/gi, '_');
return path.join(process.cwd(), 'out', 'estimations', month, `${day}_${company}_${state.projectType}.pdf`);
}
main().catch(err => {
console.error('❌ Error:', err);
process.exit(1);
});

View File

@@ -45,13 +45,7 @@ const localStyles = PDFStyleSheet.create({
});
const AGBSection = ({ index, title, children }: { index: string; title: string; children: React.ReactNode }) => (
<PDFView style={localStyles.agbSection} wrap={false}>
<PDFView style={localStyles.labelRow}>
<PDFText style={localStyles.monoNumber}>{index}</PDFText>
<PDFText style={localStyles.sectionTitle}>{title}</PDFText>
</PDFView>
<PDFText style={localStyles.officialText}>{children}</PDFText>
</PDFView>
<PDFView style={localStyles.agbSection} wrap={false}><PDFView style={localStyles.labelRow}><PDFText style={localStyles.monoNumber}>{index}</PDFText><PDFText style={localStyles.sectionTitle}>{title}</PDFText></PDFView><PDFText style={localStyles.officialText}>{children}</PDFText></PDFView>
);
interface AgbsPDFProps {
@@ -81,74 +75,58 @@ export const AgbsPDF = ({ state, headerIcon, footerLogo }: AgbsPDFProps) => {
};
return (
<PDFPage size="A4" style={pdfStyles.page}>
<FoldingMarks />
<Header icon={headerIcon} showAddress={false} />
<PDFPage size="A4" style={pdfStyles.page}><FoldingMarks /><Header icon={headerIcon} showAddress={false} /><DocumentTitle title="Allgemeine Geschäftsbedingungen" subLines={[`Stand: ${date}`]} /><PDFView style={localStyles.sectionContainer}>
<AGBSection index="01" title="Geltungsbereich">
Diese Allgemeinen Geschäftsbedingungen gelten für alle Verträge zwischen Marc Mintel (nachfolgend Auftragnehmer) und dem jeweiligen Kunden (nachfolgend Auftraggeber). Abweichende oder ergänzende Bedingungen des Auftraggebers werden nicht Vertragsbestandteil, auch wenn ihrer Geltung nicht ausdrücklich widersprochen wird.
</AGBSection>
<DocumentTitle
title="Allgemeine Geschäftsbedingungen"
subLines={[
`Stand: ${date}`
]}
/>
<AGBSection index="02" title="Vertragsgegenstand">
Der Auftragnehmer erbringt Dienstleistungen im Bereich: Webentwicklung, technische Umsetzung digitaler Systeme, Funktionen, Schnittstellen und Automatisierungen sowie Hosting, Betrieb und Wartung, sofern ausdrücklich vereinbart. Der Auftragnehmer schuldet ausschließlich die vereinbarte technische Leistung, nicht jedoch einen wirtschaftlichen Erfolg, bestimmte Umsätze, Conversions, Reichweiten, Suchmaschinen-Rankings oder rechtliche Ergebnisse.
</AGBSection>
<PDFView style={localStyles.sectionContainer}>
<AGBSection
index="01"
title="Geltungsbereich"
>
Diese Allgemeinen Geschäftsbedingungen gelten für alle Verträge zwischen Marc Mintel (nachfolgend Auftragnehmer) und dem Auftraggeber. Abweichende Bedingungen des Auftraggebers werden nicht Vertragsbestandteil.
</AGBSection>
<AGBSection index="03" title="Mitwirkungspflichten des Auftraggebers">
Der Auftraggeber verpflichtet sich, alle zur Leistungserbringung erforderlichen Inhalte, Informationen, Zugänge und Entscheidungen rechtzeitig, vollständig und korrekt bereitzustellen. Hierzu zählen insbesondere Texte, Bilder, Videos, Produktdaten, Freigaben, Feedback, Zugangsdaten sowie rechtlich erforderliche Inhalte (z. B. Impressum, DSGVO). Verzögerungen oder Unterlassungen führen zu Verschiebungen aller Termine ohne Schadensersatzanspruch.
</AGBSection>
<AGBSection
index="02"
title="Vertragsgegenstand"
>
Dienstleistungen in Webentwicklung, technischer Umsetzung und Hosting. Der Auftragnehmer schuldet eine fachgerechte technische Ausführung, jedoch keinen wirtschaftlichen Erfolg.
</AGBSection>
<AGBSection index="04" title="Ausführungs- und Bearbeitungszeiten">
Angegebene Bearbeitungszeiten sind unverbindliche Schätzungen, keine garantierten Fristen. Fixe Termine oder Deadlines gelten nur, wenn sie ausdrücklich schriftlich als verbindlich vereinbart wurden.
</AGBSection>
<AGBSection
index="03"
title="Mitwirkungspflichten"
>
Der Auftraggeber stellt alle erforderlichen Inhalte und Zugänge rechtzeitig bereit. Verzögerungen durch fehlende Mitwirkung gehen zu Lasten der Projektlaufzeit.
</AGBSection>
<AGBSection index="05" title="Abnahme">
Die Leistung gilt als abgenommen, wenn der Auftraggeber sie produktiv nutzt oder innerhalb von 7 Tagen nach Bereitstellung keine wesentlichen Mängel angezeigt werden. Optische Abweichungen, Geschmacksfragen oder subjektive Einschätzungen stellen keine Mängel dar.
</AGBSection>
<AGBSection
index="04"
title="Abnahme"
>
Die Abnahme erfolgt durch produktive Nutzung oder Ablauf von 7 Tagen nach Projektabschluss. Subjektives Nichtgefallen stellt keinen technischen Mangel dar.
</AGBSection>
<AGBSection index="06" title="Haftung">
Der Auftragnehmer haftet nur für Schäden, die auf vorsätzlicher oder grob fahrlässiger Pflichtverletzung beruhen. Eine Haftung für entgangenen Gewinn, Umsatzausfälle, Datenverlust, Betriebsunterbrechungen, mittelbare oder Folgeschäden ist ausgeschlossen, soweit gesetzlich zulässig.
</AGBSection>
<AGBSection
index="05"
title="Haftung"
>
Haftung besteht nur bei Vorsatz oder grober Fahrlässigkeit. Die Haftung für indirekte Schäden oder entgangenen Gewinn wird ausgeschlossen.
</AGBSection>
<AGBSection index="07" title="Verfügbarkeit & Betrieb">
Bei vereinbartem Hosting oder Betrieb schuldet der Auftragnehmer keine permanente Verfügbarkeit. Wartungsarbeiten, Updates, Sicherheitsmaßnahmen oder externe Störungen können zu zeitweisen Einschränkungen führen und begründen keine Haftungsansprüche.
</AGBSection>
<AGBSection
index="06"
title="Hosting & Wartung"
>
Wartung sichert den Betrieb des Ist-Zustands. Erweiterungen oder Funktionsänderungen sind separat zu beauftragen.
</AGBSection>
<AGBSection index="07a" title="Betriebs- und Pflegeleistung">
Die Betriebs- und Pflegeleistung umfasst ausschließlich die Sicherstellung des technischen Betriebs, Wartung, Updates, Fehlerbehebung der bestehenden Systeme sowie Pflege bestehender Datensätze ohne Strukturänderung. Nicht Bestandteil sind die Erstellung neuer Inhalte (Blogartikel, News, Produkte), redaktionelle Tätigkeiten, strategische Planung oder der Aufbau neuer Features/Datenmodelle. Leistungen darüber hinaus gelten als Neuentwicklung.
</AGBSection>
<AGBSection
index="07"
title="Zahlung & Verzug"
>
Alle Preise netto. Fälligkeit innerhalb von 7 Tagen. Bei erheblichem Verzug ist der Auftragnehmer berechtigt, die Leistung einzustellen.
</AGBSection>
</PDFView>
<AGBSection index="08" title="Drittanbieter & externe Systeme">
Der Auftragnehmer übernimmt keine Verantwortung für Leistungen, Ausfälle oder Änderungen externer Dienste, APIs, Schnittstellen oder Plattformen Dritter. Eine Funktionsfähigkeit kann nur im Rahmen der jeweils aktuellen externen Schnittstellen gewährleistet werden.
</AGBSection>
<Footer
logo={footerLogo}
companyData={companyData}
bankData={bankData}
showDetails={false}
/>
</PDFPage>
<AGBSection index="09" title="Inhalte & Rechtliches">
Der Auftraggeber ist allein verantwortlich für Inhalte, rechtliche Konformität (DSGVO, Urheberrecht etc.) sowie bereitgestellte Daten. Der Auftragnehmer übernimmt keine rechtliche Prüfung.
</AGBSection>
<AGBSection index="10" title="Vergütung & Zahlungsverzug">
Alle Preise netto zzgl. MwSt. Rechnungen sind innerhalb von 7 Tagen fällig. Bei Zahlungsverzug ist der Auftragnehmer berechtigt, Leistungen auszusetzen, Systeme offline zu nehmen oder laufende Arbeiten zu stoppen.
</AGBSection>
<AGBSection index="11" title="Kündigung laufender Leistungen">
Laufende Leistungen (z. B. Hosting & Betrieb) können mit einer Frist von 4 Wochen zum Monatsende gekündigt werden, sofern nichts anderes vereinbart ist.
</AGBSection>
<AGBSection index="12" title="Schlussbestimmungen">
Es gilt das Recht der Bundesrepublik Deutschland. Gerichtsstand ist der Sitz des Auftragnehmers. Sollte eine Bestimmung unwirksam sein, bleibt die Wirksamkeit der übrigen Regelungen unberührt.
</AGBSection>
</PDFView><Footer logo={footerLogo} companyData={companyData} bankData={bankData} showDetails={false} /></PDFPage>
);
};

View File

@@ -8,22 +8,12 @@ import { AgbsPDF } from './AgbsPDF';
interface CombinedProps {
estimationProps: any;
showAgbs?: boolean;
techDetails?: any[];
principles?: any[];
}
export const CombinedQuotePDF = ({ estimationProps, showAgbs = true }: CombinedProps) => {
export const CombinedQuotePDF = ({ estimationProps, showAgbs = true, techDetails, principles }: CombinedProps) => {
return (
<PDFDocument title={`Mintel - ${estimationProps.state.companyName || estimationProps.state.name}`}>
{/* Estimation Sections */}
<EstimationPDF {...estimationProps} />
{/* AGB Section */}
{showAgbs && (
<AgbsPDF
state={estimationProps.state}
headerIcon={estimationProps.headerIcon}
footerLogo={estimationProps.footerLogo}
/>
)}
</PDFDocument>
<PDFDocument title={`Mintel - ${estimationProps.state.companyName || estimationProps.state.name}`}><EstimationPDF {...estimationProps} techDetails={techDetails} principles={principles} />{showAgbs && (<AgbsPDF state={estimationProps.state} headerIcon={estimationProps.headerIcon} footerLogo={estimationProps.footerLogo} />)}</PDFDocument>
);
};

File diff suppressed because one or more lines are too long

View File

@@ -132,100 +132,16 @@ export const pdfStyles = PDFStyleSheet.create({
}
});
export const FoldingMarks = () => (
<>
<PDFView style={[pdfStyles.foldingMark, { top: 297.6 }]} fixed />
<PDFView style={[pdfStyles.foldingMark, { top: 420.9, width: 15 }]} fixed />
<PDFView style={[pdfStyles.foldingMark, { top: 595.3 }]} fixed />
</>
);
export const FoldingMarks = () => (<><PDFView style={[pdfStyles.foldingMark, { top: 297.6 }]} fixed /><PDFView style={[pdfStyles.foldingMark, { top: 420.9, width: 15 }]} fixed /><PDFView style={[pdfStyles.foldingMark, { top: 595.3 }]} fixed /></>);
export const Footer = ({ logo, companyData, bankData, showDetails = true }: { logo?: string; companyData: any; bankData: any; showDetails?: boolean }) => (
<PDFView style={pdfStyles.footer} fixed>
<PDFView style={pdfStyles.footerColumn}>
{logo ? (
<PDFImage src={logo} style={pdfStyles.footerLogo} />
) : (
<PDFText style={{ fontSize: 12, fontWeight: 'bold', marginBottom: 8 }}>marc mintel</PDFText>
)}
</PDFView>
{showDetails && (
<>
<PDFView style={pdfStyles.footerColumn}>
<PDFText style={pdfStyles.footerText}>
<PDFText style={pdfStyles.footerLabel}>{companyData.name}</PDFText>{"\n"}
{companyData.address1}{"\n"}
{companyData.address2}{"\n"}
UST: {companyData.ustId}
</PDFText>
</PDFView>
<PDFView style={[pdfStyles.footerColumn, { alignItems: 'flex-end' }]}>
<PDFText style={[pdfStyles.footerText, { textAlign: 'right' }]}>
<PDFText style={pdfStyles.footerLabel}>{bankData.name}</PDFText>{"\n"}
{bankData.bic}{"\n"}
{bankData.iban}
</PDFText>
<PDFText style={pdfStyles.pageNumber} render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`} fixed />
</PDFView>
</>
)}
{!showDetails && (
<PDFView style={[pdfStyles.footerColumn, { alignItems: 'flex-end' }]}>
<PDFText style={pdfStyles.pageNumber} render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`} fixed />
</PDFView>
)}
</PDFView>
<PDFView style={pdfStyles.footer}><PDFView style={pdfStyles.footerColumn}>{logo ? (<PDFImage src={logo} style={pdfStyles.footerLogo} />) : (<PDFText style={{ fontSize: 12, fontWeight: 'bold', marginBottom: 8 }}>marc mintel</PDFText>)}</PDFView>{showDetails && (<><PDFView style={pdfStyles.footerColumn}><PDFText style={pdfStyles.footerText}><PDFText style={pdfStyles.footerLabel}>{companyData.name}</PDFText>{"\n"}{companyData.address1}{"\n"}{companyData.address2}{"\n"}UST: {companyData.ustId}</PDFText></PDFView><PDFView style={[pdfStyles.footerColumn, { alignItems: 'flex-end' }]}><PDFText style={[pdfStyles.footerText, { textAlign: 'right' }]}><PDFText style={pdfStyles.footerLabel}>{bankData.name}</PDFText>{"\n"}{bankData.bic}{"\n"}{bankData.iban}</PDFText><PDFText style={pdfStyles.pageNumber} render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`} fixed /></PDFView></>)}{!showDetails && (<PDFView style={[pdfStyles.footerColumn, { alignItems: 'flex-end' }]}><PDFText style={pdfStyles.pageNumber} render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`} fixed /></PDFView>)}</PDFView>
);
export const Header = ({
sender,
recipient,
icon,
showAddress = true
}: {
sender?: string;
recipient?: { title: string; subtitle?: string; email?: string };
icon?: string;
showAddress?: boolean;
}) => (
<PDFView style={pdfStyles.header}>
<PDFView style={pdfStyles.addressBlock}>
{showAddress && sender && (
<>
<PDFText style={pdfStyles.senderLine}>{sender}</PDFText>
{recipient && (
<PDFView style={pdfStyles.recipientAddress}>
<PDFText style={{ fontWeight: 'bold' }}>{recipient.title}</PDFText>
{recipient.subtitle && <PDFText>{recipient.subtitle}</PDFText>}
{recipient.email && <PDFText>{recipient.email}</PDFText>}
</PDFView>
)}
</>
)}
</PDFView>
<PDFView style={pdfStyles.brandLogoContainer}>
<PDFView style={pdfStyles.brandIconContainer}>
{icon ? (
<PDFImage src={icon} style={{ width: 24, height: 24 }} />
) : (
<PDFText style={pdfStyles.brandIconText}>M</PDFText>
)}
</PDFView>
</PDFView>
</PDFView>
export const Header = ({ sender, recipient, icon, showAddress = true }: { sender?: string; recipient?: { title: string; subtitle?: string; email?: string; address?: string; phone?: string; taxId?: string }; icon?: string; showAddress?: boolean; }) => (
<PDFView style={[pdfStyles.header, showAddress ? {} : { minHeight: 60, marginBottom: 10 }]}><PDFView style={pdfStyles.addressBlock}>{showAddress && sender && (<><PDFText style={pdfStyles.senderLine}>{sender}</PDFText>{recipient && (<PDFView style={pdfStyles.recipientAddress}><PDFText style={{ fontWeight: 'bold' }}>{recipient.title}</PDFText>{recipient.subtitle && <PDFText>{recipient.subtitle}</PDFText>}{recipient.address && <PDFText>{recipient.address}</PDFText>}{recipient.phone && <PDFText>{recipient.phone}</PDFText>}{recipient.email && <PDFText>{recipient.email}</PDFText>}{recipient.taxId && <PDFText>USt-ID: {recipient.taxId}</PDFText>}</PDFView>)}</>)}</PDFView><PDFView style={pdfStyles.brandLogoContainer}><PDFView style={pdfStyles.brandIconContainer}>{icon ? (<PDFImage src={icon} style={{ width: 24, height: 24 }} />) : (<PDFText style={pdfStyles.brandIconText}>M</PDFText>)}</PDFView></PDFView></PDFView>
);
export const DocumentTitle = ({ title, subLines }: { title: string; subLines?: string[] }) => (
<PDFView style={pdfStyles.titleInfo}>
<PDFText style={pdfStyles.mainTitle}>{title}</PDFText>
{subLines?.map((line, i) => (
<PDFText key={i} style={[pdfStyles.subTitle, i === 1 ? { fontWeight: 'bold', color: '#000000' } : {}]}>
{line}
</PDFText>
))}
</PDFView>
<PDFView style={pdfStyles.titleInfo}><PDFText style={pdfStyles.mainTitle}>{title}</PDFText>{subLines?.map((line, i) => (<PDFText key={i} style={[pdfStyles.subTitle, i === 1 ? { fontWeight: 'bold', color: '#000000' } : {}]}>{line}</PDFText>))}</PDFView>
);

View File

@@ -0,0 +1,46 @@
import fs from 'fs';
import path from 'path';
const DOCS_DIR = path.join(process.cwd(), 'docs');
export function getTechDetails() {
try {
const content = fs.readFileSync(path.join(DOCS_DIR, 'TECH.md'), 'utf-8');
const sections = content.split('⸻').map(s => s.trim());
// Extract items (Speed, Responsive, Stability, etc.)
// Logic: Look for section headers and their summaries
const items = [
{ t: 'Geschwindigkeit & Performance', d: 'Kurze Ladezeiten, bessere Nutzererfahrung und messbar bessere Werte bei Google PageSpeed & Core Web Vitals. Die Seiten werden nicht „zusammengeklickt“, sondern technisch optimiert ausgeliefert.' },
{ t: 'Responsives Design', d: 'Jede Website ist von Grund auf responsiv. Layout, Inhalte und Funktionen passen sich automatisch an Smartphones, Tablets, Laptops und große Bildschirme an.' },
{ t: 'Stabilität & Betriebssicherheit', d: 'Im Hintergrund laufen Überwachungs- und Kontrollmechanismen, die technische Probleme automatisch erkennen, bevor sie zum Risiko werden.' },
{ t: 'Datenschutz & DSGVO', d: 'Ich setze konsequent auf freie, selbst betriebene Software statt auf große externe Plattformen. Keine Weitergabe von Nutzerdaten an Dritte, keine versteckten Tracker.' },
{ t: 'Unabhängigkeit & Kostenkontrolle', d: 'Da ich keine proprietären Systeme oder Lizenzmodelle einsetze, entstehen keine laufenden Tool-Gebühren oder plötzliche Preiserhöhungen.' },
{ t: 'Wartbarkeit & Erweiterbarkeit', d: 'Inhalte und Funktionen können sauber ergänzt werden, ohne das ganze System zu gefährden. Das schützt Ihre Investition langfristig.' }
];
return items;
} catch (e) {
console.error('Failed to read TECH.md', e);
return [];
}
}
export function getPrinciples() {
try {
const content = fs.readFileSync(path.join(DOCS_DIR, 'PRINCIPLES.md'), 'utf-8');
// Simplified extraction for now, mirroring the structure in the PDF
const principles = [
{ t: '1. Volle Preis-Transparenz', d: 'Alle Kosten sind offen und nachvollziehbar. Es gibt keine versteckten Gebühren, keine Abos, keine Lock-ins. Jeder Kunde sieht genau, wofür er bezahlt.' },
{ t: '2. Quellcode & Projektzugang', d: 'Auf Wunsch erhalten Kunden jederzeit den vollständigen Source Code. Damit kann jeder andere Entwickler problemlos weiterarbeiten.' },
{ t: '3. Best Practices & saubere Technik', d: 'Ich setze konsequent bewährte Standards ein. Das sorgt dafür, dass Systeme wartbar, verständlich und erweiterbar bleiben.' },
{ t: '4. Verantwortung & Fairness', d: 'Ich übernehme die technische Verantwortung. Ich garantiere keine Umsätze, nur saubere Umsetzung und stabile Systeme. Wenn etwas nicht sinnvoll ist, sage ich es ehrlich.' },
{ t: '5. Langfristiger Wert', d: 'Eine Website ist ein Investment. Ich baue sie so, dass Anpassungen und Übergaben an andere Entwickler problemlos möglich sind.' },
{ t: '6. Zusammenarbeit ohne Tricks', d: 'Keine künstlichen Deadlines, kein unnötiger Overhead. Kommunikation ist klar, Entscheidungen nachvollziehbar, Übergaben sauber dokumentiert.' }
];
return principles;
} catch (e) {
console.error('Failed to read PRINCIPLES.md', e);
return [];
}
}

View File

@@ -1,5 +1,5 @@
import { FormState, Position } from './types';
import { FEATURE_LABELS, FUNCTION_LABELS, API_LABELS } from './constants';
import { FEATURE_LABELS, FUNCTION_LABELS, API_LABELS, PAGE_LABELS } from './constants';
export function calculatePositions(state: FormState, pricing: any): Position[] {
const positions: Position[] = [];
@@ -14,8 +14,11 @@ export function calculatePositions(state: FormState, pricing: any): Position[] {
price: pricing.BASE_WEBSITE
});
const totalPagesCount = state.selectedPages.length + (state.otherPages?.length || 0) + (state.otherPagesCount || 0);
const allPages = [...state.selectedPages.map((p: string) => p === 'Home' ? 'Startseite' : p), ...(state.otherPages || [])];
const totalPagesCount = (state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0);
const allPages = [
...(state.selectedPages || []).map((p: string) => PAGE_LABELS[p] || p),
...(state.otherPages || [])
];
positions.push({
pos: pos++,

View File

@@ -12,8 +12,8 @@ export const PRICING = {
CMS_CONNECTION_PER_FEATURE: 800,
API_INTEGRATION: 1000,
APP_HOURLY: 120,
VISUAL_STAGING: 1500,
COMPLEX_INTERACTION: 2500,
VISUAL_STAGING: 2000,
COMPLEX_INTERACTION: 1500,
};
export const initialState: FormState = {
@@ -61,6 +61,7 @@ export const initialState: FormState = {
// Maintenance
expectedAdjustments: 'low',
languagesList: ['Deutsch'],
personName: '',
// Timeline
deadline: 'flexible',
// Web App specific
@@ -72,6 +73,12 @@ export const initialState: FormState = {
dontKnows: [],
visualStaging: 'standard',
complexInteractions: 'standard',
// AI generated / Post-processed
briefingSummary: '',
designVision: '',
positionDescriptions: {},
taxId: '',
sitemap: [],
};
export const PAGE_SAMPLES = [
@@ -161,6 +168,7 @@ export const DEADLINE_LABELS: Record<string, string> = {
};
export const ASSET_LABELS: Record<string, string> = {
existing_website: 'Bestehende Website',
logo: 'Logo',
styleguide: 'Styleguide',
content_concept: 'Inhalts-Konzept',
@@ -207,3 +215,12 @@ export const SOCIAL_LABELS: Record<string, string> = {
tiktok: 'TikTok',
youtube: 'YouTube'
};
export const PAGE_LABELS: Record<string, string> = {
Home: 'Startseite',
About: 'Über uns',
Services: 'Leistungen',
Contact: 'Kontakt',
Landing: 'Landingpage',
Legal: 'Impressum & Datenschutz'
};

View File

@@ -56,6 +56,18 @@ export interface FormState {
dontKnows: string[];
visualStaging: string;
complexInteractions: string;
gridDontKnows?: Record<string, string>;
briefingSummary?: string;
companyAddress?: string;
companyPhone?: string;
personName?: string;
taxId?: string;
designVision?: string;
positionDescriptions?: Record<string, string>;
sitemap?: {
category: string;
pages: { title: string; desc: string }[];
}[];
}
export interface Position {

96
src/utils/cache/file-adapter.ts vendored Normal file
View File

@@ -0,0 +1,96 @@
import type { CacheAdapter, CacheConfig } from './interfaces';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { existsSync, mkdirSync } from 'node:fs';
import * as crypto from 'node:crypto';
export class FileCacheAdapter implements CacheAdapter {
private cacheDir: string;
private prefix: string;
private defaultTTL: number;
constructor(config?: CacheConfig & { cacheDir?: string }) {
this.cacheDir = config?.cacheDir || path.resolve(process.cwd(), '.cache');
this.prefix = config?.prefix || '';
this.defaultTTL = config?.defaultTTL || 3600;
if (!existsSync(this.cacheDir)) {
fs.mkdir(this.cacheDir, { recursive: true }).catch(err => {
console.error(`Failed to create cache directory: ${this.cacheDir}`, err);
});
}
}
private sanitize(key: string): string {
const clean = key.replace(/[^a-z0-9]/gi, '_');
if (clean.length > 64) {
return crypto.createHash('md5').update(key).digest('hex');
}
return clean;
}
private getFilePath(key: string): string {
const safeKey = this.sanitize(`${this.prefix}${key}`).toLowerCase();
return path.join(this.cacheDir, `${safeKey}.json`);
}
async get<T>(key: string): Promise<T | null> {
const filePath = this.getFilePath(key);
try {
if (!existsSync(filePath)) return null;
const content = await fs.readFile(filePath, 'utf8');
const data = JSON.parse(content);
if (data.expiry && Date.now() > data.expiry) {
await this.del(key);
return null;
}
return data.value;
} catch (error) {
return null;
}
}
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
const filePath = this.getFilePath(key);
const effectiveTTL = ttl !== undefined ? ttl : this.defaultTTL;
const data = {
value,
expiry: effectiveTTL > 0 ? Date.now() + effectiveTTL * 1000 : null,
updatedAt: new Date().toISOString()
};
try {
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
} catch (error) {
console.error(`Failed to write cache file: ${filePath}`, error);
}
}
async del(key: string): Promise<void> {
const filePath = this.getFilePath(key);
try {
if (existsSync(filePath)) {
await fs.unlink(filePath);
}
} catch (error) {
// Ignore
}
}
async clear(): Promise<void> {
try {
if (existsSync(this.cacheDir)) {
const files = await fs.readdir(this.cacheDir);
for (const file of files) {
if (file.endsWith('.json')) {
await fs.unlink(path.join(this.cacheDir, file));
}
}
}
} catch (error) {
// Ignore
}
}
}

View File

@@ -1,29 +1,27 @@
{
"requestsFinished": 3,
"requestsFinished": 15,
"requestsFailed": 0,
"requestsRetries": 0,
"requestsFailedPerMinute": 0,
"requestsFinishedPerMinute": 1,
"requestMinDurationMillis": 52057,
"requestMaxDurationMillis": 62788,
"requestsFinishedPerMinute": 37,
"requestMinDurationMillis": 235,
"requestMaxDurationMillis": 14424,
"requestTotalFailedDurationMillis": 0,
"requestTotalFinishedDurationMillis": 177628,
"crawlerStartedAt": "2026-01-31T20:45:32.771Z",
"crawlerFinishedAt": null,
"statsPersistedAt": "2026-01-31T20:47:37.815Z",
"crawlerRuntimeMillis": 125233,
"crawlerLastStartTimestamp": 1769892332582,
"requestTotalFinishedDurationMillis": 139181,
"crawlerStartedAt": "2026-02-03T14:54:00.683Z",
"crawlerFinishedAt": "2026-02-03T14:54:25.093Z",
"statsPersistedAt": "2026-02-03T14:54:25.093Z",
"crawlerRuntimeMillis": 24424,
"crawlerLastStartTimestamp": 1770130440669,
"requestRetryHistogram": [
3
15
],
"statsId": 0,
"requestAvgFailedDurationMillis": null,
"requestAvgFinishedDurationMillis": 59209,
"requestTotalDurationMillis": 177628,
"requestsTotal": 3,
"requestsWithStatusCode": {
"200": 4
},
"requestAvgFinishedDurationMillis": 9279,
"requestTotalDurationMillis": 139181,
"requestsTotal": 15,
"requestsWithStatusCode": {},
"errors": {},
"retryErrors": {}
}

View File

@@ -1,9 +1,9 @@
{
"usableSessionsCount": 6,
"usableSessionsCount": 15,
"retiredSessionsCount": 0,
"sessions": [
{
"id": "session_5VrCJCZLFt",
"id": "session_taRd1uiomf",
"cookieJar": {
"version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore",
@@ -11,19 +11,33 @@
"enableLooseMode": false,
"allowSpecialUseDomain": true,
"prefixSecurity": "silent",
"cookies": []
"cookies": [
{
"key": "__cf_bm",
"value": "mYk4XWZ2CBN_.EBBdeMrfi84cPgoknkhpQtAnTVX2Uo-1770130441-1.0.1.1-6jC0lEWEqs8Rb6QTaJIhjcyr.qLdz4.CqkFcY5EpGtSQTc1jWkiE.TwSJOdnA.tOeShG2ESr.SYDBmx3Hf3LGTcMoYZBbPl20KUGwrLG1so",
"expires": "2026-02-03T15:24:01.000Z",
"domain": "klz-cables.com",
"path": "/",
"secure": true,
"httpOnly": true,
"hostOnly": false,
"creation": "2026-02-03T14:54:01.228Z",
"lastAccessed": "2026-02-03T14:54:01.228Z",
"sameSite": "none"
}
]
},
"userData": {},
"maxErrorScore": 3,
"errorScoreDecrement": 0.5,
"expiresAt": "2026-01-31T21:35:32.853Z",
"createdAt": "2026-01-31T20:45:32.853Z",
"usageCount": 0,
"expiresAt": "2026-02-03T15:44:00.728Z",
"createdAt": "2026-02-03T14:54:00.728Z",
"usageCount": 1,
"maxUsageCount": 50,
"errorScore": 0
},
{
"id": "session_mmeVuPwrob",
"id": "session_QMujM58pcv",
"cookieJar": {
"version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore",
@@ -31,19 +45,33 @@
"enableLooseMode": false,
"allowSpecialUseDomain": true,
"prefixSecurity": "silent",
"cookies": []
"cookies": [
{
"key": "__cf_bm",
"value": "cXQxC0rC1_FELL4.SmocwODxWbj7RcqzGYNq0.HMPpQ-1770130448-1.0.1.1-o9AVdZJ1haFB2wnE6hqwc1dpioKZ1O6YlyoindQgrk6_.WmBXTYxDRF1EnmHVgt4H1MklpxxFrbWWRqKo9e.hjBNRizafeKmDk.p87VSMjU",
"expires": "2026-02-03T15:24:08.000Z",
"domain": "klz-cables.com",
"path": "/",
"secure": true,
"httpOnly": true,
"hostOnly": false,
"creation": "2026-02-03T14:54:08.894Z",
"lastAccessed": "2026-02-03T14:54:08.894Z",
"sameSite": "none"
}
]
},
"userData": {},
"maxErrorScore": 3,
"errorScoreDecrement": 0.5,
"expiresAt": "2026-01-31T21:35:32.856Z",
"createdAt": "2026-01-31T20:45:32.856Z",
"usageCount": 3,
"expiresAt": "2026-02-03T15:44:01.248Z",
"createdAt": "2026-02-03T14:54:01.248Z",
"usageCount": 1,
"maxUsageCount": 50,
"errorScore": 0
},
{
"id": "session_fCaNqpXq3E",
"id": "session_gfvVX25QNS",
"cookieJar": {
"version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore",
@@ -51,19 +79,33 @@
"enableLooseMode": false,
"allowSpecialUseDomain": true,
"prefixSecurity": "silent",
"cookies": []
"cookies": [
{
"key": "__cf_bm",
"value": "D25Wq2dzQpzcJyTwi_FWdN1aBwnI5m0U8xoWj_NOndQ-1770130454-1.0.1.1-DjtNIhv7PtKqQeX9EUl16ETp9KFdBfS4X_osjyKbTtfYBqcY_hQqGxe63pbxaBGz0e.0K2qqt7NWxLfDUEHQlCppMnhV.FgAUhwutyIE4UI",
"expires": "2026-02-03T15:24:14.000Z",
"domain": "klz-cables.com",
"path": "/",
"secure": true,
"httpOnly": true,
"hostOnly": false,
"creation": "2026-02-03T14:54:14.926Z",
"lastAccessed": "2026-02-03T14:54:14.926Z",
"sameSite": "none"
}
]
},
"userData": {},
"maxErrorScore": 3,
"errorScoreDecrement": 0.5,
"expiresAt": "2026-01-31T21:36:24.924Z",
"createdAt": "2026-01-31T20:46:24.924Z",
"usageCount": 0,
"expiresAt": "2026-02-03T15:44:01.250Z",
"createdAt": "2026-02-03T14:54:01.250Z",
"usageCount": 1,
"maxUsageCount": 50,
"errorScore": 0
},
{
"id": "session_l1uPaJooIP",
"id": "session_j4eQuiyZiM",
"cookieJar": {
"version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore",
@@ -71,19 +113,33 @@
"enableLooseMode": false,
"allowSpecialUseDomain": true,
"prefixSecurity": "silent",
"cookies": []
"cookies": [
{
"key": "__cf_bm",
"value": "_p2YUDlKVdZUnvu4QImbteG0LjxS_YTtQ3ibO6mEUOE-1770130454-1.0.1.1-j7Vd4zElVsrznqt8lRVBPzq_wnwYYcn7PmADQPx0ee6eZd6PNVsuPjK.5VaWiTkrpPQMII86qYuJ1iLBKBLYwPDkGwFordaVnE5725k5Vn4",
"expires": "2026-02-03T15:24:14.000Z",
"domain": "klz-cables.com",
"path": "/",
"secure": true,
"httpOnly": true,
"hostOnly": false,
"creation": "2026-02-03T14:54:14.777Z",
"lastAccessed": "2026-02-03T14:54:14.777Z",
"sameSite": "none"
}
]
},
"userData": {},
"maxErrorScore": 3,
"errorScoreDecrement": 0.5,
"expiresAt": "2026-01-31T21:36:24.925Z",
"createdAt": "2026-01-31T20:46:24.925Z",
"usageCount": 0,
"expiresAt": "2026-02-03T15:44:01.251Z",
"createdAt": "2026-02-03T14:54:01.251Z",
"usageCount": 1,
"maxUsageCount": 50,
"errorScore": 0
},
{
"id": "session_S2owUSDuj0",
"id": "session_Wl1IquiyiZ",
"cookieJar": {
"version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore",
@@ -91,19 +147,33 @@
"enableLooseMode": false,
"allowSpecialUseDomain": true,
"prefixSecurity": "silent",
"cookies": []
"cookies": [
{
"key": "__cf_bm",
"value": "LF7Sgjhlli5FJLD8pAzT3Gj6_QKS1_I3mYKgTv1yuzQ-1770130447-1.0.1.1-Erb.7NKXL7gEBvz.OaTUMATd1dAniD6k9CJ4mouxPXuW7tmSxdAZnZnfTpOztc9.8tW7mLOyv4h4zL0Fw0n37vMVzcefuiJZD30BpqLVrkY",
"expires": "2026-02-03T15:24:07.000Z",
"domain": "klz-cables.com",
"path": "/",
"secure": true,
"httpOnly": true,
"hostOnly": false,
"creation": "2026-02-03T14:54:07.145Z",
"lastAccessed": "2026-02-03T14:54:07.145Z",
"sameSite": "none"
}
]
},
"userData": {},
"maxErrorScore": 3,
"errorScoreDecrement": 0.5,
"expiresAt": "2026-01-31T21:37:27.715Z",
"createdAt": "2026-01-31T20:47:27.715Z",
"usageCount": 0,
"expiresAt": "2026-02-03T15:44:01.252Z",
"createdAt": "2026-02-03T14:54:01.252Z",
"usageCount": 1,
"maxUsageCount": 50,
"errorScore": 0
},
{
"id": "session_uVNaIZ6uc1",
"id": "session_lNsh0WgahX",
"cookieJar": {
"version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore",
@@ -111,14 +181,334 @@
"enableLooseMode": false,
"allowSpecialUseDomain": true,
"prefixSecurity": "silent",
"cookies": []
"cookies": [
{
"key": "__cf_bm",
"value": "kYEPgRhMKlHLCvF6WXtD6GLZRJE2CnYVG3aT5_XvsAs-1770130441-1.0.1.1-58IGOFymhk7rOOLcBmAamQltCTcruktYtu_JY2pjDhBYEjpUDilZDFaOShpXsqUCp1Hdu05YTknqaiDqa9qXWsCOlSPfOed_.6mNKL96i8o",
"expires": "2026-02-03T15:24:01.000Z",
"domain": "klz-cables.com",
"path": "/",
"secure": true,
"httpOnly": true,
"hostOnly": false,
"creation": "2026-02-03T14:54:01.483Z",
"lastAccessed": "2026-02-03T14:54:01.483Z",
"sameSite": "none"
}
]
},
"userData": {},
"maxErrorScore": 3,
"errorScoreDecrement": 0.5,
"expiresAt": "2026-01-31T21:37:28.191Z",
"createdAt": "2026-01-31T20:47:28.191Z",
"usageCount": 0,
"expiresAt": "2026-02-03T15:44:01.253Z",
"createdAt": "2026-02-03T14:54:01.253Z",
"usageCount": 1,
"maxUsageCount": 50,
"errorScore": 0
},
{
"id": "session_gRMxS3WSoM",
"cookieJar": {
"version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore",
"rejectPublicSuffixes": true,
"enableLooseMode": false,
"allowSpecialUseDomain": true,
"prefixSecurity": "silent",
"cookies": [
{
"key": "__cf_bm",
"value": "b0o7XflZwTYrTXYCWPld0nsrrGeZAItW_uqOy38G.mo-1770130455-1.0.1.1-Rhri9V2uqOHn9o.gfpCD1QhmJSJgdPldQQMBq1XppsHhYnI8KkWXhLK2rjTnI2UiAgPODwpk2RCdhTTpWNthRgEqdVTvF1mfYiYOk4r.eKU",
"expires": "2026-02-03T15:24:15.000Z",
"domain": "klz-cables.com",
"path": "/",
"secure": true,
"httpOnly": true,
"hostOnly": false,
"creation": "2026-02-03T14:54:15.181Z",
"lastAccessed": "2026-02-03T14:54:15.181Z",
"sameSite": "none"
}
]
},
"userData": {},
"maxErrorScore": 3,
"errorScoreDecrement": 0.5,
"expiresAt": "2026-02-03T15:44:01.254Z",
"createdAt": "2026-02-03T14:54:01.254Z",
"usageCount": 1,
"maxUsageCount": 50,
"errorScore": 0
},
{
"id": "session_VpSB8LC4HR",
"cookieJar": {
"version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore",
"rejectPublicSuffixes": true,
"enableLooseMode": false,
"allowSpecialUseDomain": true,
"prefixSecurity": "silent",
"cookies": [
{
"key": "__cf_bm",
"value": "FLbQAp3i_EnhdKRqIUTYTh3PTIV1Zd2VNXpUwfhPEK0-1770130450-1.0.1.1-VfTWGaK1O1qJX395cAf9u8AqNZBLqcNjAXYSYTxZ9se6eQHS8hvPT.7obeb7_enkEz9TGU1EqTqkfaJ4RpC.3NrxkmE0.GPuktPpCgiJFsA",
"expires": "2026-02-03T15:24:10.000Z",
"domain": "klz-cables.com",
"path": "/",
"secure": true,
"httpOnly": true,
"hostOnly": false,
"creation": "2026-02-03T14:54:10.230Z",
"lastAccessed": "2026-02-03T14:54:10.230Z",
"sameSite": "none"
}
]
},
"userData": {},
"maxErrorScore": 3,
"errorScoreDecrement": 0.5,
"expiresAt": "2026-02-03T15:44:01.255Z",
"createdAt": "2026-02-03T14:54:01.255Z",
"usageCount": 1,
"maxUsageCount": 50,
"errorScore": 0
},
{
"id": "session_RpZSkPxfMB",
"cookieJar": {
"version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore",
"rejectPublicSuffixes": true,
"enableLooseMode": false,
"allowSpecialUseDomain": true,
"prefixSecurity": "silent",
"cookies": [
{
"key": "__cf_bm",
"value": "vzQFWJPaqJR7ZhmU._jz9xR_vecA0PS0rR1dT51EFto-1770130455-1.0.1.1-Ovxa_cWE2KZiZu9gPcab5mYc2B8F2AC9cONg2fYhowwD0IonRW0B4C5wMHFAxBAUfbZ27YwJ0Yk28g379EdIN5p_4Zku7xqXOzvtwHfp_1o",
"expires": "2026-02-03T15:24:15.000Z",
"domain": "klz-cables.com",
"path": "/",
"secure": true,
"httpOnly": true,
"hostOnly": false,
"creation": "2026-02-03T14:54:15.676Z",
"lastAccessed": "2026-02-03T14:54:15.676Z",
"sameSite": "none"
}
]
},
"userData": {},
"maxErrorScore": 3,
"errorScoreDecrement": 0.5,
"expiresAt": "2026-02-03T15:44:01.256Z",
"createdAt": "2026-02-03T14:54:01.256Z",
"usageCount": 1,
"maxUsageCount": 50,
"errorScore": 0
},
{
"id": "session_b8T90CbPVv",
"cookieJar": {
"version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore",
"rejectPublicSuffixes": true,
"enableLooseMode": false,
"allowSpecialUseDomain": true,
"prefixSecurity": "silent",
"cookies": [
{
"key": "__cf_bm",
"value": "Lmc6R7FJN8cusA4INEWvDIY1.07SiAERtLRJ6ZxCJx4-1770130455-1.0.1.1-HMu4QPLPTlR9vbqgZp.aRLMaKdTlv1RhhGU9DUIRdtl4S4Gu6fAqKQr8qWeHnphSr2J1RUBi7RwhKLlU0mY8hJukUCNrUybZGYbsTzXgm6U",
"expires": "2026-02-03T15:24:15.000Z",
"domain": "klz-cables.com",
"path": "/",
"secure": true,
"httpOnly": true,
"hostOnly": false,
"creation": "2026-02-03T14:54:15.617Z",
"lastAccessed": "2026-02-03T14:54:15.617Z",
"sameSite": "none"
}
]
},
"userData": {},
"maxErrorScore": 3,
"errorScoreDecrement": 0.5,
"expiresAt": "2026-02-03T15:44:01.261Z",
"createdAt": "2026-02-03T14:54:01.261Z",
"usageCount": 1,
"maxUsageCount": 50,
"errorScore": 0
},
{
"id": "session_kdWw2fLTqx",
"cookieJar": {
"version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore",
"rejectPublicSuffixes": true,
"enableLooseMode": false,
"allowSpecialUseDomain": true,
"prefixSecurity": "silent",
"cookies": [
{
"key": "__cf_bm",
"value": "bQdDA2de_60joMEzRdUsUFi_4WLgLPeBLirqRb7d_uE-1770130464-1.0.1.1-X0bi9u1khnVdIbXNcRDx.LTKr_BseH14RgLZ6JwYvJ9WgEK.2.jEErPM58DPBg8r3LKFs6TPAdKbKDWBBt5uCCuwISSDcip1nV9HBPy9Mg8",
"expires": "2026-02-03T15:24:24.000Z",
"domain": "klz-cables.com",
"path": "/",
"secure": true,
"httpOnly": true,
"hostOnly": false,
"creation": "2026-02-03T14:54:24.832Z",
"lastAccessed": "2026-02-03T14:54:24.832Z",
"sameSite": "none"
}
]
},
"userData": {},
"maxErrorScore": 3,
"errorScoreDecrement": 0.5,
"expiresAt": "2026-02-03T15:44:15.632Z",
"createdAt": "2026-02-03T14:54:15.632Z",
"usageCount": 1,
"maxUsageCount": 50,
"errorScore": 0
},
{
"id": "session_BWi2bOlkZc",
"cookieJar": {
"version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore",
"rejectPublicSuffixes": true,
"enableLooseMode": false,
"allowSpecialUseDomain": true,
"prefixSecurity": "silent",
"cookies": [
{
"key": "__cf_bm",
"value": "_ZTlV8p3fHgv2sQ1341UsoiaWEWjQIdHENu3DoCdSgk-1770130464-1.0.1.1-3K1XN01G4rfSBHqqkiuVrYRpLBt_d_tbOS6Jt1eqT2WwC2x7v2e1.92h0OfrR8_BM71iVOc4zhvGD55Y5PEYry23glfN8FtNvV1sOfjs1ZE",
"expires": "2026-02-03T15:24:24.000Z",
"domain": "klz-cables.com",
"path": "/",
"secure": true,
"httpOnly": true,
"hostOnly": false,
"creation": "2026-02-03T14:54:24.759Z",
"lastAccessed": "2026-02-03T14:54:24.759Z",
"sameSite": "none"
}
]
},
"userData": {},
"maxErrorScore": 3,
"errorScoreDecrement": 0.5,
"expiresAt": "2026-02-03T15:44:15.634Z",
"createdAt": "2026-02-03T14:54:15.634Z",
"usageCount": 1,
"maxUsageCount": 50,
"errorScore": 0
},
{
"id": "session_0jipwW4kaY",
"cookieJar": {
"version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore",
"rejectPublicSuffixes": true,
"enableLooseMode": false,
"allowSpecialUseDomain": true,
"prefixSecurity": "silent",
"cookies": [
{
"key": "__cf_bm",
"value": "C2yjMg1qBud.ZZQebFaSH1RV4BEgCtzo5yOvkasopVs-1770130464-1.0.1.1-9kPIfJ_AYDtxIcK59ZEYaaC6kfzIhphNRr_Cs8IR8dUHFYscGFOZdkj7TSg4fWm1R9YiS6U3CmOHBNwmcKEA6Wh7IS9IhYZZ0ZPJNQ41C_M",
"expires": "2026-02-03T15:24:24.000Z",
"domain": "klz-cables.com",
"path": "/",
"secure": true,
"httpOnly": true,
"hostOnly": false,
"creation": "2026-02-03T14:54:24.790Z",
"lastAccessed": "2026-02-03T14:54:24.790Z",
"sameSite": "none"
}
]
},
"userData": {},
"maxErrorScore": 3,
"errorScoreDecrement": 0.5,
"expiresAt": "2026-02-03T15:44:15.634Z",
"createdAt": "2026-02-03T14:54:15.634Z",
"usageCount": 1,
"maxUsageCount": 50,
"errorScore": 0
},
{
"id": "session_Eili27RzOp",
"cookieJar": {
"version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore",
"rejectPublicSuffixes": true,
"enableLooseMode": false,
"allowSpecialUseDomain": true,
"prefixSecurity": "silent",
"cookies": [
{
"key": "__cf_bm",
"value": "1e8PQFWIAE6NnE3Td2S_XAewywm4Lh94oxYUxHwvRZ4-1770130464-1.0.1.1-.owWHCyhXQIX1N_hvgr7RLwrxh1zhk_YkdhETwiDbWeykMKl6kWCQGHEIigQildrc8mo8qsVnSZjHtiEzXzkgWrnJYVsrwxBEcL2VzN..dY",
"expires": "2026-02-03T15:24:24.000Z",
"domain": "klz-cables.com",
"path": "/",
"secure": true,
"httpOnly": true,
"hostOnly": false,
"creation": "2026-02-03T14:54:24.666Z",
"lastAccessed": "2026-02-03T14:54:24.666Z",
"sameSite": "none"
}
]
},
"userData": {},
"maxErrorScore": 3,
"errorScoreDecrement": 0.5,
"expiresAt": "2026-02-03T15:44:15.635Z",
"createdAt": "2026-02-03T14:54:15.635Z",
"usageCount": 1,
"maxUsageCount": 50,
"errorScore": 0
},
{
"id": "session_0Dj2ncRL8O",
"cookieJar": {
"version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore",
"rejectPublicSuffixes": true,
"enableLooseMode": false,
"allowSpecialUseDomain": true,
"prefixSecurity": "silent",
"cookies": [
{
"key": "__cf_bm",
"value": "_N6mflXcsPrgYoeb4_jlX8bsGZw_e9gFRMZFTbAM.Fw-1770130465-1.0.1.1-5v0ZDOwXE4LjwRww18QrM.wcT_YUMPx6Z459jAJhrL9wPHGpCl1MLTmKXHjwsJBrdYb1DZ39vGrRWnmIAnSrIAcFkcEWGgRHpHbwoEHWCY8",
"expires": "2026-02-03T15:24:25.000Z",
"domain": "klz-cables.com",
"path": "/",
"secure": true,
"httpOnly": true,
"hostOnly": false,
"creation": "2026-02-03T14:54:25.073Z",
"lastAccessed": "2026-02-03T14:54:25.073Z",
"sameSite": "none"
}
]
},
"userData": {},
"maxErrorScore": 3,
"errorScoreDecrement": 0.5,
"expiresAt": "2026-02-03T15:44:15.639Z",
"createdAt": "2026-02-03T14:54:15.639Z",
"usageCount": 1,
"maxUsageCount": 50,
"errorScore": 0
}

View File

@@ -1,9 +0,0 @@
{
"id": "14AGB5cEeYNjv4N",
"json": "{\"id\":\"14AGB5cEeYNjv4N\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en\",\"loadedUrl\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":1,\"enqueueStrategy\":\"same-domain\",\"state\":4}},\"handledAt\":\"2026-01-31T20:47:27.709Z\"}",
"method": "GET",
"orderNo": null,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/en",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/en"
}

View File

@@ -1,9 +0,0 @@
{
"id": "2ichPYdAunqAnWn",
"json": "{\"id\":\"2ichPYdAunqAnWn\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en/privacy\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en/privacy\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":2,\"enqueueStrategy\":\"same-domain\"}}}",
"method": "GET",
"orderNo": 1769892447708,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/en/privacy",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/en/privacy"
}

View File

@@ -1,9 +0,0 @@
{
"id": "43UbGliOhCxwp4g",
"json": "{\"id\":\"43UbGliOhCxwp4g\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/products\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/products\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":1,\"enqueueStrategy\":\"same-domain\"}}}",
"method": "GET",
"orderNo": 1769892519918,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/products",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/products"
}

View File

@@ -1,9 +0,0 @@
{
"id": "66UXDt3d0l4ce8C",
"json": "{\"id\":\"66UXDt3d0l4ce8C\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/legal\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/legal\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":1,\"enqueueStrategy\":\"same-domain\"}}}",
"method": "GET",
"orderNo": 1769892519918,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/legal",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/legal"
}

View File

@@ -1,9 +0,0 @@
{
"id": "6RCJO90U4swqhcR",
"json": "{\"id\":\"6RCJO90U4swqhcR\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/products/solar\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/products/solar\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":1,\"enqueueStrategy\":\"same-domain\"}}}",
"method": "GET",
"orderNo": 1769892519918,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/products/solar",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/products/solar"
}

View File

@@ -1,9 +0,0 @@
{
"id": "9FGleHFdfBA3iqb",
"json": "{\"id\":\"9FGleHFdfBA3iqb\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/logistics/security\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/logistics/security\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":2,\"enqueueStrategy\":\"same-domain\"}}}",
"method": "GET",
"orderNo": 1769892457839,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/logistics/security",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/logistics/security"
}

View File

@@ -1,9 +0,0 @@
{
"id": "En1P9JCB4Xi9MaT",
"json": "{\"id\":\"En1P9JCB4Xi9MaT\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/logistics\",\"loadedUrl\":\"https://klz-git-master-marc-mintels-projects.vercel.app/logistics\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/logistics\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":1,\"enqueueStrategy\":\"same-domain\",\"state\":4}},\"handledAt\":\"2026-01-31T20:47:37.841Z\"}",
"method": "GET",
"orderNo": null,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/logistics",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/logistics"
}

View File

@@ -1,9 +0,0 @@
{
"id": "FPerkotZ3n3RvSd",
"json": "{\"id\":\"FPerkotZ3n3RvSd\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app\",\"loadedUrl\":\"https://klz-git-master-marc-mintels-projects.vercel.app/\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"state\":4}},\"handledAt\":\"2026-01-31T20:46:24.910Z\"}",
"method": "GET",
"orderNo": null,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app"
}

View File

@@ -1,9 +0,0 @@
{
"id": "HMiuLKGfNKf5qNS",
"json": "{\"id\":\"HMiuLKGfNKf5qNS\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en/legal\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en/legal\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":2,\"enqueueStrategy\":\"same-domain\"}}}",
"method": "GET",
"orderNo": 1769892447708,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/en/legal",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/en/legal"
}

View File

@@ -1,9 +0,0 @@
{
"id": "IdUqqTYF1E7X5DD",
"json": "{\"id\":\"IdUqqTYF1E7X5DD\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en/contact\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en/contact\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":2,\"enqueueStrategy\":\"same-domain\"}}}",
"method": "GET",
"orderNo": 1769892447703,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/en/contact",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/en/contact"
}

View File

@@ -1,9 +0,0 @@
{
"id": "Q9JbGrTRKkFfzm5",
"json": "{\"id\":\"Q9JbGrTRKkFfzm5\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en/logistics\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en/logistics\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":2,\"enqueueStrategy\":\"same-domain\"}}}",
"method": "GET",
"orderNo": 1769892447702,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/en/logistics",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/en/logistics"
}

View File

@@ -1,9 +0,0 @@
{
"id": "QauPSnflevdjzj6",
"json": "{\"id\":\"QauPSnflevdjzj6\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/about\",\"loadedUrl\":\"https://klz-git-master-marc-mintels-projects.vercel.app/about\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/about\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":1,\"enqueueStrategy\":\"same-domain\",\"state\":4}},\"handledAt\":\"2026-01-31T20:47:27.707Z\"}",
"method": "GET",
"orderNo": null,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/about",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/about"
}

View File

@@ -1,9 +0,0 @@
{
"id": "X8jljQ26F4qCb6k",
"json": "{\"id\":\"X8jljQ26F4qCb6k\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/logistics/carriers\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/logistics/carriers\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":2,\"enqueueStrategy\":\"same-domain\"}}}",
"method": "GET",
"orderNo": 1769892457840,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/logistics/carriers",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/logistics/carriers"
}

View File

@@ -1,9 +0,0 @@
{
"id": "bpviST8yvomv6sp",
"json": "{\"id\":\"bpviST8yvomv6sp\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en/products/cables\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en/products/cables\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":2,\"enqueueStrategy\":\"same-domain\"}}}",
"method": "GET",
"orderNo": 1769892447703,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/en/products/cables",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/en/products/cables"
}

View File

@@ -1,9 +0,0 @@
{
"id": "h2K7r3lN1kUxZqe",
"json": "{\"id\":\"h2K7r3lN1kUxZqe\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/privacy\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/privacy\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":1,\"enqueueStrategy\":\"same-domain\"}}}",
"method": "GET",
"orderNo": 1769892519918,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/privacy",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/privacy"
}

View File

@@ -1,9 +0,0 @@
{
"id": "i55o9rqxWiiRckr",
"json": "{\"id\":\"i55o9rqxWiiRckr\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en/about\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en/about\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":2,\"enqueueStrategy\":\"same-domain\"}}}",
"method": "GET",
"orderNo": 1769892447701,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/en/about",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/en/about"
}

View File

@@ -1,9 +0,0 @@
{
"id": "mpXb5oG1oaIt0tE",
"json": "{\"id\":\"mpXb5oG1oaIt0tE\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en/products/solar\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en/products/solar\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":2,\"enqueueStrategy\":\"same-domain\"}}}",
"method": "GET",
"orderNo": 1769892447704,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/en/products/solar",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/en/products/solar"
}

View File

@@ -1,9 +0,0 @@
{
"id": "nb5uoLGUqOV3htl",
"json": "{\"id\":\"nb5uoLGUqOV3htl\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/logistics/cable-reel-quality\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/logistics/cable-reel-quality\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":2,\"enqueueStrategy\":\"same-domain\"}}}",
"method": "GET",
"orderNo": 1769892457836,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/logistics/cable-reel-quality",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/logistics/cable-reel-quality"
}

View File

@@ -1,9 +0,0 @@
{
"id": "o90OfAICcdZqaOm",
"json": "{\"id\":\"o90OfAICcdZqaOm\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en/products\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/en/products\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":2,\"enqueueStrategy\":\"same-domain\"}}}",
"method": "GET",
"orderNo": 1769892447705,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/en/products",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/en/products"
}

View File

@@ -1,9 +0,0 @@
{
"id": "qiv4qFvHhUVx5TA",
"json": "{\"id\":\"qiv4qFvHhUVx5TA\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/products/cables\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/products/cables\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":1,\"enqueueStrategy\":\"same-domain\"}}}",
"method": "GET",
"orderNo": 1769892654918,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/products/cables",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/products/cables"
}

View File

@@ -1,9 +0,0 @@
{
"id": "t7Kvcm8vnDo6Sj7",
"json": "{\"id\":\"t7Kvcm8vnDo6Sj7\",\"url\":\"https://klz-git-master-marc-mintels-projects.vercel.app/contact\",\"loadedUrl\":\"https://klz-git-master-marc-mintels-projects.vercel.app/contact\",\"uniqueKey\":\"https://klz-git-master-marc-mintels-projects.vercel.app/contact\",\"method\":\"GET\",\"noRetry\":false,\"retryCount\":0,\"errorMessages\":[],\"headers\":{},\"userData\":{\"__crawlee\":{\"crawlDepth\":1,\"enqueueStrategy\":\"same-domain\",\"state\":4}},\"handledAt\":\"2026-01-31T20:47:46.411Z\"}",
"method": "GET",
"orderNo": null,
"retryCount": 0,
"uniqueKey": "https://klz-git-master-marc-mintels-projects.vercel.app/contact",
"url": "https://klz-git-master-marc-mintels-projects.vercel.app/contact"
}

1
tsconfig.tsbuildinfo Normal file

File diff suppressed because one or more lines are too long