From ce421eb8d24a604eb2dc01c12ebb46dbaed3fcb6 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Tue, 3 Feb 2026 22:35:01 +0100 Subject: [PATCH] refactor: Centralize PDF styling with `COLORS` and `FONT_SIZES` and enhance module content and dynamic title generation. --- scripts/ai-estimate.ts | 90 +++++----- src/components/EstimationPDF.tsx | 33 ++-- src/components/pdf/SharedUI.tsx | 161 ++++++++++++------ src/components/pdf/SimpleLayout.tsx | 3 +- .../pdf/modules/BrandingModules.tsx | 48 +++--- src/components/pdf/modules/BriefingModule.tsx | 18 +- src/components/pdf/modules/CommonModules.tsx | 22 +-- .../pdf/modules/EstimationModule.tsx | 6 +- .../pdf/modules/FrontPageModule.tsx | 62 ++++--- src/components/pdf/modules/SitemapModule.tsx | 24 +-- src/logic/pricing/calculator.ts | 15 +- .../default/SDK_CRAWLER_STATISTICS_0.json | 22 +-- .../default/SDK_SESSION_POOL_STATE.json | 96 +++++------ 13 files changed, 346 insertions(+), 254 deletions(-) diff --git a/scripts/ai-estimate.ts b/scripts/ai-estimate.ts index f749882..ebc4f1f 100644 --- a/scripts/ai-estimate.ts +++ b/scripts/ai-estimate.ts @@ -271,12 +271,12 @@ Output language: GERMAN (Strict). Focus 100% on the BRIEFING text provided by the user. Use the DISTILLED_CRAWL only as background context for terms or company details. If there is a conflict, the BRIEFING is the absolute source of truth. ### 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. -- Map to internal IDs for selectedPages, features, functions, apiSystems, assets. -- Identify if isRelaunch is true (briefing mentions existing site or URL). +- Extract **companyName**: The full legal and brand name (e.g., "E-TIB GmbH"). Use signatures and crawl data. +- Extract **personName**: The name of the primary human contact (e.g., "Danny Joseph"). **CRITICAL**: Check email signatures and "Mit freundlichen Grüßen" blocks. DO NOT use "Sie", "Firma" or generic terms if a name exists. +- Extract **existingWebsite**: The primary URL mentioned in the briefing or signature (e.g., "www.e-tib.com"). +- Extract **websiteTopic**: A short descriptor of the CORE BUSINESS (e.g., "Kabeltiefbau"). MAX 3 WORDS. +- **isRelaunch**: Set to TRUE if the briefing mentions an existing website, a URL, or if the company is an established entity (e.g. "Gruppe", "GmbH seit 20XX"). Assume a presence exists that needs a modern "Zentrale Webpräsenz". +- **CRITICAL LOGIC**: If a URL is mentioned, isRelaunch MUST be TRUE. - For all textual values (deadline, websiteTopic, targetAudience etc.): USE GERMAN. - **multilang**: ONLY if the briefing mentions multiple target languages (e.g., DE/EN). - **maps**: If "Google Maps" or location maps are mentioned or implicit (Contact page). @@ -295,6 +295,7 @@ Focus 100% on the BRIEFING text provided by the user. Use the DISTILLED_CRAWL on "companyName": string, "companyAddress": string, "personName": string, + "existingWebsite": string, "websiteTopic": string, "isRelaunch": boolean, "selectedPages": string[], @@ -305,7 +306,7 @@ Focus 100% on the BRIEFING text provided by the user. Use the DISTILLED_CRAWL on "deadline": string (GERMAN), "targetAudience": "B2B" | "B2C" | "Internal" | string (GERMAN), "expectedAdjustments": "low" | "medium" | "high" | string (GERMAN), - "employeeCount": string + "employeeCount": "ca. 10+" | "ca. 50+" | "ca. 100+" | "ca. 250+" | "ca. 500+" | "ca. 1000+" } `; const pass1UserPrompt = `BRIEFING (TRUTH SOURCE):\n${briefing}\n\nCOMMENTS:\n${comments}\n\nDISTILLED_CRAWL (CONTEXT ONLY):\n${distilledCrawl}`; @@ -352,25 +353,26 @@ ${JSON.stringify(facts, null, 2)} // 3. PASS 3: Strategic Content (Bespoke Strategy) console.log(' ↳ Pass 3: Strategic Content (Bespoke Strategy)...'); const pass3SystemPrompt = ` -You are a high-end Digital Architect. Analyze the BRIEFING. -ABSOLUTE RULE: OUTPUT MUST BE 100% GERMAN. +You are a high-end Digital Architect. Your goal is to make the CUSTOMER feel 100% understood. +Analyze the BRIEFING and the EXISTING WEBSITE context. -### TONE & COMMUNICATION PRINCIPLES (MANDATORY): +### TONE & COMMUNICATION PRINCIPLES (STRICT): ${tone} ### OBJECTIVE: -1. **briefingSummary**: Summarize the project's essence for the CUSTOMER. - - FOLLOW PRINCIPLE 1, 5 & 6: Clear, direct, no marketing fluff. - - **TONE**: Write naturally in the Ich-Form. Avoid starting every sentence with "Ich". - - **MIRROR TEST**: Capture unique customer "hooks" or personality. - - Focus 100% on the BRIEFING (TRUTH SOURCE). -2. **designVision**: A solid, grounded, and high-quality description of the look & feel. - - FOLLOW PRINCIPLE 1, 3 & 6: Fact-based, professional, high density of information. - - **TONE**: Natural Ich-Form. Focus on the execution and technical decisions. - - **BESPOKE ELEMENTS**: If the client mentions specific layout ideas, incorporate these. - - **NO ARROGANCE**: Eliminate all "high-end", "world-class" language. - - **SIMPLE & CLEAR**: Use simple German. No buzzwords. - - 3-5 sentences of deep analysis. +1. **briefingSummary**: A deep, respectful summary of the status quo and the target state. + - **LENGTH**: EXACTLY TWO PARAGRAPHS. Minimum 8 sentences total. + - **MIRROR TEST**: Acknowledge the EXISTING website specifically. Why does the new project make sense NOW? + - **ABSOLUTE RULE**: DO NOT claim "keine digitale Repräsentation", "erstmals abgebildet", "Erstplatzierung" or "kommunikative Lücke" regarding existence if Pass 1 identified this as a RELAUNCH (isRelaunch=true). EXPLICITLY acknowledge the existing context and the NEED FOR EVOLUTION/MODERNIZATION. + - **RESPECT**: Explicitly incorporate the customer's expressed wishes. + - **ABSOLUTE RULE**: DO NOT INVENT DETAILS. Do not mention specific people (e.g., "Frieder Helmich"), software versions, or internal details NOT present in the briefing. + - **TONE**: Natural Ich-Form. Clear, direct, zero marketing fluff. +2. **designVision**: A high-density, professional vision of the future execution. + - **LENGTH**: EXACTLY TWO PARAGRAPHS. Minimum 6 sentences total. + - **INVESTMENT VALUE**: Plant a clear picture of a stable, high-quality system. + - **TECHNICAL PRECISION**: Focus on execution (Typografie, Visual Logic, Performance). + - **NO FLUFF**: Do NOT focus on "Full-Screen Hero Video" as the main thing. Focus on the FIRM's essence and how we translate it into a professional tool. + - **ABSOLUTE RULE**: NO HALLUCINATIONS. Stay general yet precise. No "Verzicht auf Stockmaterial" unless explicitly stated. ### OUTPUT FORMAT (Strict JSON): { @@ -380,7 +382,10 @@ ${tone} `; 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 (TRUTH SOURCE):\n${briefing}` }], + messages: [ + { role: 'system', content: pass3SystemPrompt }, + { role: 'user', content: `BRIEFING (TRUTH SOURCE):\n${briefing}\n\nEXISTING WEBSITE (CONTEXT):\n${distilledCrawl}\n\nEXTRACTED FACTS:\n${JSON.stringify(facts, null, 2)}` } + ], response_format: { type: 'json_object' } }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); addUsage(p3Resp.data); @@ -455,10 +460,12 @@ Each position in the quote must be perfectly justified and detailed. - **BAD**: "Ich programmiere Scroll-Effekte." - **GOOD**: "Visuelle Inszenierung der Meilensteine durch Scroll-aktivierte Timeline-Elemente." -### FORBIDDEN PHRASES: -- "Erweiterte Formulare", "Verschiedene Funktionen", "Allgemeine Logik", "Optimierte Darstellung", "Individuelle Formular-Logik". - -11. **NO "MARKETING LINGO"**: Never say "avoids branding" or "maximizes performance". Say "Implements HTML5 Video Player". ALWAYS DESCRIBE THE TASK. +### POSITION RULES (STRICT): +1. **Basis Website Setup**: This position MUST ALWAYS contain exactly these 7 points: "Projekt-Setup & Infrastruktur, Hosting-Bereitstellung, Grundstruktur & Design-Vorlage, technisches SEO-Basics, Analytics (mit automatischem Mail-Report), Testing-, Staging- & Production-Umgebung, Livegang." +2. **Sorglos-Betrieb (Hosting)**: Describe the service (Hosting, SSL, Security-Updates, 24/7 Monitoring, Portfolio-Update-Service). NEVER mention "Inklusive Basis-Infrastruktur". +3. **LOGIC CONSISTENCY**: If Pass 1 extracted 1 function, you MUST describe exactly 1 function scope. If you describe two things (e.g., "Formular AND Search") but the count is 1, it is a FAIL. +4. **SIMPLICITY**: Write in "Simple German". High density of information, but easy for a CEO. No jargon. +5. **NO IMPLEMENTATION DETAILS**: Focus on WHAT is done, not HOW (no libraries, no technical "under-the-hood" talk). ### DATA CONTEXT: ${JSON.stringify({ facts, details, strategy, ia }, null, 2)} @@ -483,23 +490,15 @@ You are the "Industrial Critic". Your goal is to catch quality regressions and e Analyze the CURRENT_STATE against the BRIEFING_TRUTH. ### CRITICAL ERROR CHECKLIST (FAIL IF FOUND): -1. **Placeholder Leakage**: Catch "null", "undefined", or generic strings like "Verschiedene Funktionen", "Erweiterte Formulare". -2. **Detail Loss**: The user mentioned specific terms. Are they present? If not, ADD THEM. -3. **Consistency**: Ensure the count of pages in "Individuelle Seiten" matches the sitemap pages. -4. **Deadlines**: Ensure relative dates (e.g., "April / Mai") are resolved to the year 2026. -5. **TONE & WORDING FAILURE**: - - FAIL if "Ich" or "Mein" is used in positionDescriptions. - - FAIL if "wir" or "unser" is used anywhere. - - FAIL if a sentence in briefingSummary is too long or marketing-heavy. - - FAIL if "möglicherweise", "grundsätzlich", "in der Regel" is used. - - FAIL if passive voice ("es wird") is used. -6. **MAPPING FAILURE**: - - FAIL if visual elements (Scroll-Effekte, Slider, Hover) are in "Logik-Funktionen". They MUST be in "Visuelle Inszenierung". - - FAIL if multi-step forms or calculators are in "Logik-Funktionen". They MUST be in "Komplexe Interaktion". -7. **PROMPT REWRITE**: If you find any of these errors, rewrite the field entirely to be 100% compliant. +1. **Hallucination Leakage**: FAIL if names of people (e.g., "Frieder Helmich"), specific software versions, or invented details are used unless they appear EXACTLY in the BRIEFING. + - **CRITICAL**: Forbid "Sie", "Ansprechpartner" or "Unternehmen" for personName if a name IS in the briefing. If none is in briefing, use empty string. +2. **Logic Conflict**: FAIL if isRelaunch is true but briefingSummary claims no website exists or uses phrases like "Da aktuell keine digitale Repräsentation vorliegt", "erstmals abgebildet", "Erstplatzierung" or "Lücke schließen" (regarding existence). + - FAIL if the description in positionDescriptions mentions more items than extracted in facts. +3. **Implementation Fluff**: FAIL if "React", "Next.js", "TypeScript", "Tailwind" or other tech-stack details are mentioned. Focus on Concept & Result. +4. **Length Check**: Briefing and Vision MUST be significantly long (EXACTLY 2 paragraphs each, minimum 8 sentences for briefing, 6 for vision). ### MISSION: -Return updated fields ONLY. Specifically focus on hardening 'positionDescriptions', 'sitemap', and 'briefingSummary'. +Return updated fields ONLY. Specifically focus on hardening 'positionDescriptions', 'sitemap', 'briefingSummary', and 'designVision'. ### DATA CONTEXT: ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)} @@ -527,7 +526,12 @@ ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)} }; const cleanedReflection = unwrap(reflection); - return { ...result, ...cleanedReflection }; + Object.entries(cleanedReflection).forEach(([key, value]) => { + if (value && value !== "" && value !== "null") { + result[key] = value; + } + }); + return result; }; let finalState = mergeReflection({ diff --git a/src/components/EstimationPDF.tsx b/src/components/EstimationPDF.tsx index 984afe7..e3a5a6b 100644 --- a/src/components/EstimationPDF.tsx +++ b/src/components/EstimationPDF.tsx @@ -78,43 +78,50 @@ export const EstimationPDF = ({ } // Full Portfolio Mode + let pageCounter = 1; + const getPageNum = () => (pageCounter++).toString().padStart(2, '0'); + return ( <> - + - + {state.sitemap && state.sitemap.length > 0 && ( - + )} - + - + - - - + {techDetails && techDetails.length > 0 && ( + + + + )} - - - + {principles && principles.length > 0 && ( + + + + )} - + - + diff --git a/src/components/pdf/SharedUI.tsx b/src/components/pdf/SharedUI.tsx index 467746f..4a92094 100644 --- a/src/components/pdf/SharedUI.tsx +++ b/src/components/pdf/SharedUI.tsx @@ -1,23 +1,51 @@ 'use client'; import * as React from 'react'; -import { - Text as PDFText, - View as PDFView, - StyleSheet as PDFStyleSheet, - Image as PDFImage -} from '@react-pdf/renderer'; +import { View as PDFView, Text as PDFText, StyleSheet, Link as PDFLink, Image as PDFImage, Font } from '@react-pdf/renderer'; -export const pdfStyles = PDFStyleSheet.create({ +// INDUSTRIAL DESIGN SYSTEM TOKENS +export const COLORS = { + CHARCOAL: '#0f172a', // Slate 900 + TEXT_MAIN: '#334155', // Slate 700 + TEXT_DIM: '#64748b', // Slate 500 + TEXT_LIGHT: '#94a3b8', // Slate 400 + DIVIDER: '#cbd5e1', // Slate 300 + GRID: '#f1f5f9', // Slate 100 + BLUEPRINT: '#e2e8f0', // Slate 200 + WHITE: '#ffffff' +}; + +export const FONT_SIZES = { + H1: 24, + H2: 18, + H3: 12, + BODY: 9, + TINY: 7, + SUB: 8, + BLUEPRINT: 5 +}; + +// Register a more technical font if possible, or use Helvetica with varying weights +// Note: helvetica-bold is standard in react-pdf + +export const pdfStyles = StyleSheet.create({ page: { paddingTop: 45, // DIN 5008 paddingLeft: 70, // ~25mm paddingRight: 57, // ~20mm paddingBottom: 80, // Safe buffer for absolute footer - backgroundColor: '#ffffff', + backgroundColor: COLORS.WHITE, fontFamily: 'Helvetica', - fontSize: 10, - color: '#000000', + fontSize: FONT_SIZES.BODY, + color: COLORS.CHARCOAL, + }, + titlePage: { + width: '100%', + height: '100%', + backgroundColor: COLORS.WHITE, + fontFamily: 'Helvetica', + color: COLORS.CHARCOAL, + padding: 0, // NO PADDING to prevent inner overflow page breaks }, header: { flexDirection: 'row', @@ -31,13 +59,13 @@ export const pdfStyles = PDFStyleSheet.create({ marginTop: 45, // DIN 5008 positioning for window }, senderLine: { - fontSize: 7, + fontSize: FONT_SIZES.TINY, textDecoration: 'underline', - color: '#666666', + color: COLORS.TEXT_DIM, marginBottom: 8, }, recipientAddress: { - fontSize: 10, + fontSize: FONT_SIZES.BODY, lineHeight: 1.4, }, brandLogoContainer: { @@ -47,14 +75,14 @@ export const pdfStyles = PDFStyleSheet.create({ brandIconContainer: { width: 40, height: 40, - backgroundColor: '#000000', + backgroundColor: '#0f172a', borderRadius: 8, alignItems: 'center', justifyContent: 'center', marginBottom: 12, }, brandIconText: { - color: '#ffffff', + color: COLORS.WHITE, fontSize: 20, fontWeight: 'bold', }, @@ -62,27 +90,27 @@ export const pdfStyles = PDFStyleSheet.create({ marginBottom: 24, }, mainTitle: { - fontSize: 12, + fontSize: FONT_SIZES.H3, fontWeight: 'bold', marginBottom: 4, - color: '#000000', + color: COLORS.CHARCOAL, textTransform: 'uppercase', letterSpacing: 1, }, subTitle: { - fontSize: 9, - color: '#666666', + fontSize: FONT_SIZES.BODY, + color: COLORS.TEXT_DIM, marginTop: 2, }, section: { marginBottom: 32, }, sectionTitle: { - fontSize: 8, + fontSize: FONT_SIZES.SUB, fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 1, - color: '#999999', + color: COLORS.TEXT_LIGHT, marginBottom: 8, }, footer: { @@ -91,7 +119,7 @@ export const pdfStyles = PDFStyleSheet.create({ left: 70, right: 57, borderTopWidth: 1, - borderTopColor: '#f1f5f9', + borderTopColor: COLORS.GRID, paddingTop: 16, flexDirection: 'row', justifyContent: 'space-between', @@ -108,17 +136,17 @@ export const pdfStyles = PDFStyleSheet.create({ marginBottom: 8, }, footerText: { - fontSize: 7, - color: '#94a3b8', + fontSize: FONT_SIZES.TINY, + color: COLORS.TEXT_LIGHT, lineHeight: 1.5, }, footerLabel: { fontWeight: 'bold', - color: '#64748b', + color: COLORS.TEXT_DIM, }, pageNumber: { - fontSize: 7, - color: '#cbd5e1', + fontSize: FONT_SIZES.TINY, + color: COLORS.DIVIDER, fontWeight: 'bold', marginTop: 8, textAlign: 'right', @@ -128,14 +156,41 @@ export const pdfStyles = PDFStyleSheet.create({ left: 20, width: 10, borderTopWidth: 0.5, - borderTopColor: '#cbd5e1', + borderTopColor: COLORS.DIVIDER, }, divider: { width: '100%', height: 1, - backgroundColor: '#f1f5f9', + backgroundColor: COLORS.DIVIDER, marginVertical: 12, }, + blueprintGrid: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + zIndex: -10, + }, + gridLineH: { + width: '100%', + height: 0.5, + backgroundColor: COLORS.GRID, + position: 'absolute', + }, + gridLineV: { + width: 0.5, + height: '100%', + backgroundColor: COLORS.GRID, + position: 'absolute', + }, + technicalMarker: { + position: 'absolute', + fontSize: FONT_SIZES.BLUEPRINT, + color: COLORS.BLUEPRINT, + fontFamily: 'Helvetica', + letterSpacing: 1, + }, // Atoms industrialListItem: { flexDirection: 'row', @@ -145,47 +200,47 @@ export const pdfStyles = PDFStyleSheet.create({ industrialBulletBox: { width: 6, height: 6, - backgroundColor: '#0f172a', + backgroundColor: COLORS.CHARCOAL, marginRight: 8, marginTop: 5, }, industrialTitle: { - fontSize: 24, + fontSize: FONT_SIZES.H1, fontWeight: 'bold', - color: '#0f172a', + color: COLORS.CHARCOAL, marginBottom: 6, - letterSpacing: -0.5, + letterSpacing: 0, // Reset for clarity }, industrialSubtitle: { - fontSize: 8, + fontSize: FONT_SIZES.SUB, fontWeight: 'bold', - color: '#94a3b8', + color: COLORS.TEXT_LIGHT, textTransform: 'uppercase', marginBottom: 16, letterSpacing: 2, }, industrialTextLead: { - fontSize: 10, - color: '#334155', + fontSize: FONT_SIZES.BODY, + color: COLORS.TEXT_MAIN, lineHeight: 1.6, marginBottom: 12, }, industrialText: { - fontSize: 9, - color: '#64748b', + fontSize: FONT_SIZES.BODY, + color: COLORS.TEXT_DIM, lineHeight: 1.6, marginBottom: 8, }, industrialCard: { padding: 16, borderWidth: 1, - borderColor: '#e2e8f0', + borderColor: COLORS.BLUEPRINT, marginBottom: 12, }, industrialCardTitle: { - fontSize: 10, + fontSize: FONT_SIZES.BODY + 1, // 10 fontWeight: 'bold', - color: '#0f172a', + color: COLORS.CHARCOAL, marginBottom: 4, textTransform: 'uppercase', letterSpacing: 0.5, @@ -193,22 +248,30 @@ export const pdfStyles = PDFStyleSheet.create({ darkBox: { marginTop: 32, padding: 24, - backgroundColor: '#0f172a', - color: '#ffffff', + backgroundColor: COLORS.CHARCOAL, + color: COLORS.WHITE, }, darkTitle: { - fontSize: 18, + fontSize: FONT_SIZES.H2, fontWeight: 'bold', - color: '#ffffff', + color: COLORS.WHITE, marginBottom: 8, }, darkText: { - fontSize: 9, - color: '#94a3b8', + fontSize: FONT_SIZES.BODY, + color: COLORS.TEXT_LIGHT, lineHeight: 1.6, }, }); +const styles = pdfStyles; + +export const BlueprintBackground = () => ( + + {/* Clean background - grid lines removed per user request */} + +); + export const IndustrialListItem = ({ children }: { children: React.ReactNode }) => ( @@ -238,5 +301,5 @@ export const Header = ({ sender, recipient, icon, showAddress = true }: { sender ); export const DocumentTitle = ({ title, subLines }: { title: string; subLines?: string[] }) => ( - {title}{subLines?.map((line, i) => ({line}))} + {title}{subLines?.map((line, i) => ({line}))} ); diff --git a/src/components/pdf/SimpleLayout.tsx b/src/components/pdf/SimpleLayout.tsx index 5a954ff..d507de2 100644 --- a/src/components/pdf/SimpleLayout.tsx +++ b/src/components/pdf/SimpleLayout.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { Page as PDFPage, View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer'; -import { Header, Footer, pdfStyles } from './SharedUI'; +import { Header, Footer, pdfStyles, BlueprintBackground } from './SharedUI'; const simpleStyles = StyleSheet.create({ industrialPage: { @@ -47,6 +47,7 @@ export const SimpleLayout = ({ }: SimpleLayoutProps) => { return ( +
{pageNumber && {pageNumber}} diff --git a/src/components/pdf/modules/BrandingModules.tsx b/src/components/pdf/modules/BrandingModules.tsx index 8f55768..613bf13 100644 --- a/src/components/pdf/modules/BrandingModules.tsx +++ b/src/components/pdf/modules/BrandingModules.tsx @@ -2,22 +2,22 @@ import * as React from 'react'; import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer'; -import { IndustrialListItem, IndustrialCard, Divider } from '../SharedUI'; +import { IndustrialListItem, IndustrialCard, Divider, COLORS, FONT_SIZES } from '../SharedUI'; const styles = StyleSheet.create({ - industrialTitle: { fontSize: 24, fontWeight: 'bold', color: '#0f172a', marginBottom: 6, letterSpacing: -0.5 }, - industrialSubtitle: { fontSize: 8, fontWeight: 'bold', color: '#94a3b8', textTransform: 'uppercase', marginBottom: 16, letterSpacing: 2 }, - industrialTextLead: { fontSize: 10, color: '#334155', lineHeight: 1.6, marginBottom: 12 }, - industrialText: { fontSize: 9, color: '#64748b', lineHeight: 1.6, marginBottom: 8 }, + industrialTitle: { fontSize: FONT_SIZES.H1, fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 6, letterSpacing: -1 }, + industrialSubtitle: { fontSize: FONT_SIZES.SUB, fontWeight: 'bold', color: COLORS.TEXT_LIGHT, textTransform: 'uppercase', marginBottom: 16, letterSpacing: 2 }, + industrialTextLead: { fontSize: FONT_SIZES.H3, color: COLORS.TEXT_MAIN, lineHeight: 1.6, marginBottom: 12 }, + industrialText: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_DIM, lineHeight: 1.6, marginBottom: 8 }, industrialGrid2: { flexDirection: 'row', gap: 32 }, industrialCol: { width: '48%' }, - darkBox: { marginTop: 32, padding: 24, backgroundColor: '#0f172a', color: '#ffffff' }, - darkTitle: { fontSize: 18, fontWeight: 'bold', color: '#ffffff', marginBottom: 8 }, - darkText: { fontSize: 9, color: '#94a3b8', lineHeight: 1.6 }, + darkBox: { marginTop: 32, padding: 24, backgroundColor: COLORS.CHARCOAL, color: COLORS.WHITE }, + darkTitle: { fontSize: FONT_SIZES.H2, fontWeight: 'bold', color: COLORS.WHITE, marginBottom: 8 }, + darkText: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_LIGHT, lineHeight: 1.6 }, industrialBulletBox: { width: 6, height: 6, - backgroundColor: '#0f172a', + backgroundColor: COLORS.CHARCOAL, marginRight: 8, marginTop: 5, }, @@ -27,16 +27,16 @@ export const AboutModule = () => ( <> Über mich Direkt. Sauber. Verantwortlich. - + - Ich entwickle Websysteme seit 15 Jahren. Ich kenne Agenturen, Konzerne und Startups. Ich arbeite alleine, weil ich Verantwortung nicht teilen will. - Technik scheitert selten an Bits und Bytes. Sie scheitert an unklaren Zuständigkeiten. Ich bin Ihr einziger Ansprechpartner. Ich treffe die Entscheidungen und ich löse die Probleme. + Seit 15 Jahren entstehen unter meiner Leitung Websysteme für Agenturen, Konzerne und Startups. Ich arbeite bewusst alleine, um die volle Verantwortung für jedes Projekt zu tragen. + Technik scheitert selten an Bits und Bytes, sondern meist an unklaren Zuständigkeiten. Als Ihr direkter Ansprechpartner treffe ich die notwendigen Entscheidungen und löse technische Probleme ohne Umwege. Mein Standard: - Ich liefere Code ohne Altlasten. - Ich baue Systeme, die ohne mich laufen. - Ich arbeite ohne Overhead und Stille Post. + Code-Lieferung ohne technische Altlasten. + Entwicklung von Systemen, die autark operieren. + Verzicht auf Overhead und Kommunikationsverluste. @@ -45,10 +45,10 @@ export const AboutModule = () => ( Ich antworte direkt auf technische Fragen. Ich garantiere die Umsetzung. - - Keine Projektmanager. - Keine Vertriebler. - Kein Ticket-Chaos. + + Keine Projektmanager. + Keine Vertriebler. + Kein Ticket-Chaos. @@ -64,16 +64,16 @@ export const CrossSellModule = ({ state }: any) => { <> {title} {subtitle} - + {isWebsite ? ( <> - Ich identifiziere manuelle Abläufe in Ihrem Unternehmen und ersetze sie durch Software. Ich eliminiere Tippfehler und Zeitfresser. - Ich schaffe Zeit für wertschöpfende Arbeit. Ich digitalisiere das Chaos. + Manuelle Abläufe binden Kapazitäten. Durch maßgeschneiderte Software ersetze ich fehleranfällige Prozesse und eliminiere Zeitfresser in Ihrem Unternehmen. + Ziel ist der Gewinn wertvoller Arbeitszeit. Digitalisierung ordnet das Chaos. - Direktes Feedback - Ich prüfe Ihren Prozess innerhalb von 48 Stunden. Ich sage Ihnen sofort, ob eine Automatisierung wirtschaftlich sinnvoll ist. + Individuelle Prüfung + Ich analysiere Ihren spezifischen Prozess auf technisches Automatisierungspotenzial. Das Ergebnis liefert Klarheit darüber, ob eine Umsetzung wirtschaftlich sinnvoll ist. diff --git a/src/components/pdf/modules/BriefingModule.tsx b/src/components/pdf/modules/BriefingModule.tsx index d2dda5f..2221d2a 100644 --- a/src/components/pdf/modules/BriefingModule.tsx +++ b/src/components/pdf/modules/BriefingModule.tsx @@ -2,16 +2,16 @@ import * as React from 'react'; import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer'; -import { DocumentTitle } from '../SharedUI'; +import { DocumentTitle, COLORS, FONT_SIZES } from '../SharedUI'; const styles = StyleSheet.create({ section: { marginBottom: 24 }, - sectionTitle: { fontSize: 10, fontWeight: 'bold', marginBottom: 8, color: '#0f172a' }, + sectionTitle: { fontSize: FONT_SIZES.BODY + 1, fontWeight: 'bold', marginBottom: 8, color: COLORS.CHARCOAL }, configGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 6, marginTop: 6 }, configItem: { width: '24%', marginBottom: 4 }, - configLabel: { fontSize: 5, color: '#94a3b8', textTransform: 'uppercase', marginBottom: 2 }, - configValue: { fontSize: 7, color: '#0f172a', fontWeight: 'bold' }, - visionText: { fontSize: 9, color: '#334155', lineHeight: 1.8, textAlign: 'justify' }, + configLabel: { fontSize: FONT_SIZES.BLUEPRINT, color: COLORS.TEXT_LIGHT, textTransform: 'uppercase', marginBottom: 2 }, + configValue: { fontSize: FONT_SIZES.TINY, color: COLORS.CHARCOAL, fontWeight: 'bold' }, + visionText: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_MAIN, lineHeight: 1.8, textAlign: 'justify' }, }); export const BriefingModule = ({ state }: any) => ( @@ -26,15 +26,15 @@ export const BriefingModule = ({ state }: any) => ( Kern-Informationen - Ansprechpartner{state.personName || "Sie"} - Projektart{state.statusQuo || (state.existingWebsite ? 'Relaunch' : 'Neuentwicklung')} + Kontakt{state.personName || "—"} + Projekttyp{state.isRelaunch ? 'Website Evolution' : (state.statusQuo || 'Neukonzeption')} Mitarbeiter{state.employeeCount || "—"} Zeitplan{state.deadline || 'Flexibel'} {state.designVision && ( - - Strategische Vision + + Strategische Vision {state.designVision} )} diff --git a/src/components/pdf/modules/CommonModules.tsx b/src/components/pdf/modules/CommonModules.tsx index f8dd7fe..818ed46 100644 --- a/src/components/pdf/modules/CommonModules.tsx +++ b/src/components/pdf/modules/CommonModules.tsx @@ -2,16 +2,16 @@ import * as React from 'react'; import { View as PDFView, Text as PDFText, StyleSheet, Image as PDFImage } from '@react-pdf/renderer'; -import { DocumentTitle, Divider } from '../SharedUI'; +import { DocumentTitle, Divider, COLORS, FONT_SIZES } from '../SharedUI'; const styles = StyleSheet.create({ section: { marginBottom: 24 }, pricingGrid: { marginTop: 24 }, - pricingRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: '#f1f5f9', paddingVertical: 10, alignItems: 'flex-start' }, - pricingTitle: { width: '30%', fontSize: 9, fontWeight: 'bold', color: '#0f172a' }, - pricingDesc: { width: '55%', fontSize: 8, color: '#64748b', lineHeight: 1.4 }, - pricingTag: { width: '15%', fontSize: 9, fontWeight: 'bold', textAlign: 'right' }, - configLabel: { fontSize: 5, color: '#94a3b8', textTransform: 'uppercase', marginBottom: 8 }, + pricingRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: COLORS.DIVIDER, paddingVertical: 10, alignItems: 'flex-start' }, + pricingTitle: { width: '30%', fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.CHARCOAL }, + pricingDesc: { width: '55%', fontSize: FONT_SIZES.SUB, color: COLORS.TEXT_DIM, lineHeight: 1.4 }, + pricingTag: { width: '15%', fontSize: FONT_SIZES.BODY, fontWeight: 'bold', textAlign: 'right', color: COLORS.CHARCOAL }, + configLabel: { fontSize: FONT_SIZES.BLUEPRINT, color: COLORS.TEXT_LIGHT, textTransform: 'uppercase', marginBottom: 8 }, }); const CHROME_ICON = '/Users/marcmintel/Projects/mintel.me/src/assets/browser/chrome.png'; // Fallback to a placeholder if not found @@ -21,7 +21,7 @@ export const techPageModule = ({ techDetails, headerIcon }: any) => ( <> - Ich entwickle Websites als moderne, performante Websysteme. + Ich entwickle Websites als moderne, performante Websysteme. {techDetails?.map((item: any, i: number) => ( @@ -38,9 +38,9 @@ export const TransparenzModule = ({ pricing }: any) => ( <> - Festpreise statt Stundenabrechnung - Ich biete Planungssicherheit. Ich kalkuliere nach einem modularen Festpreis-System. Sie zahlen für Ergebnisse, nicht für die Zeit. Ich schließe versteckte Kosten aus. - + Festpreise statt Stundenabrechnung + Ich biete Planungssicherheit. Ich kalkuliere nach einem modularen Festpreis-System. Sie zahlen für Ergebnisse, nicht für die Zeit. Ich schließe versteckte Kosten aus. + @@ -72,7 +72,7 @@ export const TransparenzModule = ({ pricing }: any) => ( 6. Integrationen Ich binde Drittsysteme wie CRM, ERP oder Stripe an. Ich richte CMS-Schnittstellen zur unabhängigen Inhaltsverwaltung ein. - ab {pricing.API_INTEGRATION?.toLocaleString('de-DE')} € + ab {pricing.API_INTEGRATION?.toLocaleString('de-DE')} € / Stück 7. Betrieb (12 Monate) diff --git a/src/components/pdf/modules/EstimationModule.tsx b/src/components/pdf/modules/EstimationModule.tsx index cbd1dc9..6ddea63 100644 --- a/src/components/pdf/modules/EstimationModule.tsx +++ b/src/components/pdf/modules/EstimationModule.tsx @@ -6,7 +6,7 @@ import { DocumentTitle } from '../SharedUI'; const styles = StyleSheet.create({ table: { marginTop: 12 }, - tableHeader: { flexDirection: 'row', paddingBottom: 8, borderBottomWidth: 1, borderBottomColor: '#000000', marginBottom: 12 }, + tableHeader: { flexDirection: 'row', paddingBottom: 8, borderBottomWidth: 1, borderBottomColor: '#334155', marginBottom: 12 }, tableRow: { flexDirection: 'row', paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: '#f8fafc', alignItems: 'flex-start' }, colPos: { width: '8%' }, colDesc: { width: '62%' }, @@ -17,11 +17,11 @@ const styles = StyleSheet.create({ itemTitle: { fontSize: 10, fontWeight: 'bold', marginBottom: 4 }, itemDesc: { fontSize: 8, color: '#666666', lineHeight: 1.4 }, priceText: { fontSize: 10, fontWeight: 'bold' }, - summaryContainer: { borderTopWidth: 1, borderTopColor: '#000000', paddingTop: 8 }, + summaryContainer: { borderTopWidth: 1, borderTopColor: '#334155', paddingTop: 8 }, summaryRow: { flexDirection: 'row', justifyContent: 'flex-end', paddingVertical: 4, alignItems: 'baseline' }, summaryLabel: { fontSize: 7, color: '#64748b', textTransform: 'uppercase', letterSpacing: 1, fontWeight: 'bold', marginRight: 12 }, summaryValue: { fontSize: 9, fontWeight: 'bold', width: 100, textAlign: 'right' }, - totalRow: { flexDirection: 'row', justifyContent: 'flex-end', paddingTop: 12, marginTop: 8, borderTopWidth: 2, borderTopColor: '#000000', alignItems: 'baseline' }, + totalRow: { flexDirection: 'row', justifyContent: 'flex-end', paddingTop: 12, marginTop: 8, borderTopWidth: 2, borderTopColor: '#334155', alignItems: 'baseline' }, }); export const EstimationModule = ({ state, positions, totalPrice, date }: any) => ( diff --git a/src/components/pdf/modules/FrontPageModule.tsx b/src/components/pdf/modules/FrontPageModule.tsx index 4c396e6..aa7e05b 100644 --- a/src/components/pdf/modules/FrontPageModule.tsx +++ b/src/components/pdf/modules/FrontPageModule.tsx @@ -2,18 +2,20 @@ import * as React from 'react'; import { View as PDFView, Text as PDFText, Image as PDFImage, StyleSheet } from '@react-pdf/renderer'; +import { COLORS, FONT_SIZES } from '../SharedUI'; const styles = StyleSheet.create({ titlePage: { + flex: 1, // Fill the whole page padding: 60, justifyContent: 'center', alignItems: 'center', - height: '90%', + backgroundColor: COLORS.WHITE, }, titleBrandIcon: { width: 80, height: 80, - backgroundColor: '#000000', + backgroundColor: COLORS.CHARCOAL, borderRadius: 16, alignItems: 'center', justifyContent: 'center', @@ -21,53 +23,59 @@ const styles = StyleSheet.create({ }, brandIconText: { fontSize: 40, - color: '#ffffff', + color: COLORS.WHITE, fontWeight: 'bold' }, titleProjectName: { - fontSize: 24, + fontSize: FONT_SIZES.H1, fontWeight: 'bold', - color: '#0f172a', + color: COLORS.CHARCOAL, marginBottom: 16, textAlign: 'center', - textTransform: 'uppercase', - letterSpacing: 2, + maxWidth: '85%', + lineHeight: 1.2, }, titleCustomerName: { - fontSize: 14, - color: '#64748b', + fontSize: FONT_SIZES.H3, + color: COLORS.TEXT_DIM, marginBottom: 40, textAlign: 'center', + maxWidth: '80%', }, titleDocumentType: { - fontSize: 10, - color: '#94a3b8', + fontSize: FONT_SIZES.BODY + 1, // ~10 + color: COLORS.TEXT_LIGHT, textTransform: 'uppercase', letterSpacing: 4, - marginBottom: 8, + marginBottom: 12, }, titleDivider: { width: 40, height: 2, - backgroundColor: '#000000', + backgroundColor: COLORS.CHARCOAL, marginBottom: 40, }, titleDate: { - fontSize: 9, - color: '#94a3b8', - marginTop: 'auto', + fontSize: FONT_SIZES.BODY, + color: COLORS.TEXT_LIGHT, + marginTop: 40, }, }); -export const FrontPageModule = ({ state, headerIcon, date }: any) => ( - - - {headerIcon ? : M} +export const FrontPageModule = ({ state, headerIcon, date }: any) => { + const fullTitle = `Digitale Webpräsenz für\n${state.companyName || "Ihr Projekt"}`; + + // Responsive font size based on length + const fontSize = fullTitle.length > 60 ? 14 : fullTitle.length > 40 ? 18 : 22; + + return ( + + + {headerIcon ? : M} + + {fullTitle} + + {date} | Marc Mintel - Konzept & Kostenschätzung - {state.projectType === 'website' ? 'Digitale Präsenz' : 'Digitale Applikation'} - - für {state.companyName || "Ihr Projekt"} - {date} | Marc Mintel - -); + ); +}; diff --git a/src/components/pdf/modules/SitemapModule.tsx b/src/components/pdf/modules/SitemapModule.tsx index acb4f15..0e2c296 100644 --- a/src/components/pdf/modules/SitemapModule.tsx +++ b/src/components/pdf/modules/SitemapModule.tsx @@ -2,32 +2,32 @@ import * as React from 'react'; import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer'; -import { DocumentTitle, Divider } from '../SharedUI'; +import { DocumentTitle, Divider, COLORS, FONT_SIZES } from '../SharedUI'; const styles = StyleSheet.create({ section: { marginBottom: 24 }, sitemapTree: { marginTop: 20 }, sitemapRootNode: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 }, - sitemapRootDot: { width: 6, height: 6, borderRadius: 3, backgroundColor: '#000000', marginRight: 10 }, - sitemapRootTitle: { fontSize: 10, fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 1 }, - sitemapMainLine: { position: 'absolute', left: 2, top: 20, bottom: 0, width: 0.5, backgroundColor: '#cbd5e1' }, + sitemapRootDot: { width: 6, height: 6, borderRadius: 3, backgroundColor: COLORS.CHARCOAL, marginRight: 10 }, + sitemapRootTitle: { fontSize: FONT_SIZES.H3, fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 1, color: COLORS.CHARCOAL }, + sitemapMainLine: { position: 'absolute', left: 2, top: 20, bottom: 0, width: 0.5, backgroundColor: COLORS.DIVIDER }, sitemapBranch: { marginLeft: 20, marginBottom: 12, position: 'relative' }, sitemapNode: { flexDirection: 'row', alignItems: 'center', marginBottom: 4 }, - sitemapRootIcon: { width: 4, height: 4, backgroundColor: '#000000', marginRight: 8 }, - sitemapBranchTitle: { fontSize: 8, fontWeight: 'bold' }, - sitemapLeaf: { marginLeft: 12, borderLeftWidth: 0.5, borderLeftColor: '#cbd5e1', paddingLeft: 12, marginTop: 4 }, + sitemapRootIcon: { width: 4, height: 4, backgroundColor: COLORS.CHARCOAL, marginRight: 8 }, + sitemapBranchTitle: { fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.TEXT_MAIN }, + sitemapLeaf: { marginLeft: 12, borderLeftWidth: 0.5, borderLeftColor: COLORS.DIVIDER, paddingLeft: 12, marginTop: 4 }, sitemapLeafNode: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 6 }, - sitemapLeafPointer: { fontSize: 7, color: '#94a3b8', marginRight: 6 }, - sitemapLeafTitle: { fontSize: 7, fontWeight: 'bold' }, - sitemapLeafDesc: { fontSize: 6, color: '#64748b', lineHeight: 1.3, marginTop: 1 }, + sitemapLeafPointer: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_LIGHT, marginRight: 6 }, + sitemapLeafTitle: { fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.TEXT_MAIN }, + sitemapLeafDesc: { fontSize: FONT_SIZES.TINY, color: COLORS.TEXT_DIM, lineHeight: 1.3, marginTop: 1 }, }); export const SitemapModule = ({ state }: any) => ( <> - Die folgende Struktur bildet das Fundament für die Benutzerführung und Informationsarchitektur Ihres Projekts. - + Die folgende Struktur bildet das Fundament für die Benutzerführung und Informationsarchitektur Ihres Projekts. + {state.websiteTopic || 'Digitales Ökosystem'} diff --git a/src/logic/pricing/calculator.ts b/src/logic/pricing/calculator.ts index fef4931..ad0fe94 100644 --- a/src/logic/pricing/calculator.ts +++ b/src/logic/pricing/calculator.ts @@ -14,16 +14,25 @@ export function calculatePositions(state: FormState, pricing: any): Position[] { price: pricing.BASE_WEBSITE }); - const totalPagesCount = (state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0); + const sitemapPagesCount = state.sitemap?.reduce((acc: number, cat: any) => acc + (cat.pages?.length || 0), 0) || 0; + const totalPagesCount = Math.max( + (state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0), + sitemapPagesCount + ); + const allPages = [ ...(state.selectedPages || []).map((p: string) => PAGE_LABELS[p] || p), - ...(state.otherPages || []) + ...(state.otherPages || []), + ...(state.sitemap?.flatMap((cat: any) => cat.pages?.map((p: any) => p.title)) || []) ]; + // Deduplicate labels + const uniquePages = Array.from(new Set(allPages)); + positions.push({ pos: pos++, title: 'Individuelle Seiten', - desc: `Gestaltung und Umsetzung von ${totalPagesCount} individuellen Seiten-Layouts (${allPages.join(', ')}).`, + desc: `Gestaltung und Umsetzung von ${totalPagesCount} individuellen Seiten-Layouts (${uniquePages.join(', ')}).`, qty: totalPagesCount, price: totalPagesCount * pricing.PAGE }); diff --git a/storage/key_value_stores/default/SDK_CRAWLER_STATISTICS_0.json b/storage/key_value_stores/default/SDK_CRAWLER_STATISTICS_0.json index 08b127e..a40d2ab 100644 --- a/storage/key_value_stores/default/SDK_CRAWLER_STATISTICS_0.json +++ b/storage/key_value_stores/default/SDK_CRAWLER_STATISTICS_0.json @@ -3,23 +3,23 @@ "requestsFailed": 0, "requestsRetries": 0, "requestsFailedPerMinute": 0, - "requestsFinishedPerMinute": 142, - "requestMinDurationMillis": 287, - "requestMaxDurationMillis": 1881, + "requestsFinishedPerMinute": 158, + "requestMinDurationMillis": 610, + "requestMaxDurationMillis": 2310, "requestTotalFailedDurationMillis": 0, - "requestTotalFinishedDurationMillis": 11907, - "crawlerStartedAt": "2026-02-03T18:37:56.387Z", - "crawlerFinishedAt": "2026-02-03T18:37:59.745Z", - "statsPersistedAt": "2026-02-03T18:37:59.745Z", - "crawlerRuntimeMillis": 3370, - "crawlerLastStartTimestamp": 1770143876375, + "requestTotalFinishedDurationMillis": 12652, + "crawlerStartedAt": "2026-02-03T18:57:19.097Z", + "crawlerFinishedAt": "2026-02-03T18:57:22.128Z", + "statsPersistedAt": "2026-02-03T18:57:22.128Z", + "crawlerRuntimeMillis": 3044, + "crawlerLastStartTimestamp": 1770145039084, "requestRetryHistogram": [ 8 ], "statsId": 0, "requestAvgFailedDurationMillis": null, - "requestAvgFinishedDurationMillis": 1488, - "requestTotalDurationMillis": 11907, + "requestAvgFinishedDurationMillis": 1582, + "requestTotalDurationMillis": 12652, "requestsTotal": 8, "requestsWithStatusCode": {}, "errors": {}, diff --git a/storage/key_value_stores/default/SDK_SESSION_POOL_STATE.json b/storage/key_value_stores/default/SDK_SESSION_POOL_STATE.json index 9f2fc1d..fd55904 100644 --- a/storage/key_value_stores/default/SDK_SESSION_POOL_STATE.json +++ b/storage/key_value_stores/default/SDK_SESSION_POOL_STATE.json @@ -3,7 +3,7 @@ "retiredSessionsCount": 0, "sessions": [ { - "id": "session_cHq4dMfwXI", + "id": "session_4ERnLfhRfe", "cookieJar": { "version": "tough-cookie@6.0.0", "storeType": "MemoryCookieStore", @@ -14,28 +14,28 @@ "cookies": [ { "key": "8a164f127e89bfa6ad5b54e0547581b9", - "value": "pjedkv3ggc43er8m2edt6i3gqi", + "value": "7faeptahkgpngts9h84iishemk", "domain": "www.schleicher-gruppe.de", "path": "/", "secure": true, "httpOnly": true, "hostOnly": true, - "creation": "2026-02-03T18:37:57.817Z", - "lastAccessed": "2026-02-03T18:37:57.817Z" + "creation": "2026-02-03T18:57:19.776Z", + "lastAccessed": "2026-02-03T18:57:19.776Z" } ] }, "userData": {}, "maxErrorScore": 3, "errorScoreDecrement": 0.5, - "expiresAt": "2026-02-03T19:27:56.432Z", - "createdAt": "2026-02-03T18:37:56.432Z", + "expiresAt": "2026-02-03T19:47:19.140Z", + "createdAt": "2026-02-03T18:57:19.140Z", "usageCount": 1, "maxUsageCount": 50, "errorScore": 0 }, { - "id": "session_YbphnNZDOG", + "id": "session_b0TzOu99YB", "cookieJar": { "version": "tough-cookie@6.0.0", "storeType": "MemoryCookieStore", @@ -46,28 +46,28 @@ "cookies": [ { "key": "8a164f127e89bfa6ad5b54e0547581b9", - "value": "ja7mdd60clptutmne95nsea0f4", + "value": "dis65ncl0oggk09keriqi85sc3", "domain": "www.schleicher-gruppe.de", "path": "/", "secure": true, "httpOnly": true, "hostOnly": true, - "creation": "2026-02-03T18:37:58.124Z", - "lastAccessed": "2026-02-03T18:37:58.124Z" + "creation": "2026-02-03T18:57:20.400Z", + "lastAccessed": "2026-02-03T18:57:20.400Z" } ] }, "userData": {}, "maxErrorScore": 3, "errorScoreDecrement": 0.5, - "expiresAt": "2026-02-03T19:27:57.842Z", - "createdAt": "2026-02-03T18:37:57.842Z", + "expiresAt": "2026-02-03T19:47:19.796Z", + "createdAt": "2026-02-03T18:57:19.796Z", "usageCount": 1, "maxUsageCount": 50, "errorScore": 0 }, { - "id": "session_j4wCqQrHhE", + "id": "session_FQDoGeShzw", "cookieJar": { "version": "tough-cookie@6.0.0", "storeType": "MemoryCookieStore", @@ -78,28 +78,28 @@ "cookies": [ { "key": "8a164f127e89bfa6ad5b54e0547581b9", - "value": "bl0s9u6150j1fe56qq5p81raur", + "value": "aa5n3vgkgl8tg6dbn5vimrkr60", "domain": "www.schleicher-gruppe.de", "path": "/", "secure": true, "httpOnly": true, "hostOnly": true, - "creation": "2026-02-03T18:37:59.132Z", - "lastAccessed": "2026-02-03T18:37:59.132Z" + "creation": "2026-02-03T18:57:21.228Z", + "lastAccessed": "2026-02-03T18:57:21.228Z" } ] }, "userData": {}, "maxErrorScore": 3, "errorScoreDecrement": 0.5, - "expiresAt": "2026-02-03T19:27:57.844Z", - "createdAt": "2026-02-03T18:37:57.844Z", + "expiresAt": "2026-02-03T19:47:19.798Z", + "createdAt": "2026-02-03T18:57:19.798Z", "usageCount": 1, "maxUsageCount": 50, "errorScore": 0 }, { - "id": "session_km9kX8juX5", + "id": "session_7qPl1RuIhU", "cookieJar": { "version": "tough-cookie@6.0.0", "storeType": "MemoryCookieStore", @@ -110,28 +110,28 @@ "cookies": [ { "key": "8a164f127e89bfa6ad5b54e0547581b9", - "value": "7kb8am8on70lsg0498u61mjh80", + "value": "h98q2ebuq62iuei16vj2gr27p9", "domain": "www.schleicher-gruppe.de", "path": "/", "secure": true, "httpOnly": true, "hostOnly": true, - "creation": "2026-02-03T18:37:59.617Z", - "lastAccessed": "2026-02-03T18:37:59.617Z" + "creation": "2026-02-03T18:57:21.562Z", + "lastAccessed": "2026-02-03T18:57:21.562Z" } ] }, "userData": {}, "maxErrorScore": 3, "errorScoreDecrement": 0.5, - "expiresAt": "2026-02-03T19:27:57.845Z", - "createdAt": "2026-02-03T18:37:57.845Z", + "expiresAt": "2026-02-03T19:47:19.799Z", + "createdAt": "2026-02-03T18:57:19.799Z", "usageCount": 1, "maxUsageCount": 50, "errorScore": 0 }, { - "id": "session_0LdWEWTmbd", + "id": "session_nT83yclMaD", "cookieJar": { "version": "tough-cookie@6.0.0", "storeType": "MemoryCookieStore", @@ -142,28 +142,28 @@ "cookies": [ { "key": "8a164f127e89bfa6ad5b54e0547581b9", - "value": "67m4qccak2or4g35v85dghk7lo", + "value": "64s6fh151m5i2vqe61qpe4slpq", "domain": "www.schleicher-gruppe.de", "path": "/", "secure": true, "httpOnly": true, "hostOnly": true, - "creation": "2026-02-03T18:37:59.603Z", - "lastAccessed": "2026-02-03T18:37:59.603Z" + "creation": "2026-02-03T18:57:21.682Z", + "lastAccessed": "2026-02-03T18:57:21.682Z" } ] }, "userData": {}, "maxErrorScore": 3, "errorScoreDecrement": 0.5, - "expiresAt": "2026-02-03T19:27:57.846Z", - "createdAt": "2026-02-03T18:37:57.846Z", + "expiresAt": "2026-02-03T19:47:19.800Z", + "createdAt": "2026-02-03T18:57:19.800Z", "usageCount": 1, "maxUsageCount": 50, "errorScore": 0 }, { - "id": "session_7tIybVMGvQ", + "id": "session_U8c4xgDdgZ", "cookieJar": { "version": "tough-cookie@6.0.0", "storeType": "MemoryCookieStore", @@ -174,28 +174,28 @@ "cookies": [ { "key": "8a164f127e89bfa6ad5b54e0547581b9", - "value": "9tvsa0um2dbqqt64us08bjpugt", + "value": "fmep2i6s7s8hvdakvkv6gsfpqb", "domain": "www.schleicher-gruppe.de", "path": "/", "secure": true, "httpOnly": true, "hostOnly": true, - "creation": "2026-02-03T18:37:59.563Z", - "lastAccessed": "2026-02-03T18:37:59.563Z" + "creation": "2026-02-03T18:57:21.862Z", + "lastAccessed": "2026-02-03T18:57:21.862Z" } ] }, "userData": {}, "maxErrorScore": 3, "errorScoreDecrement": 0.5, - "expiresAt": "2026-02-03T19:27:57.847Z", - "createdAt": "2026-02-03T18:37:57.847Z", + "expiresAt": "2026-02-03T19:47:19.801Z", + "createdAt": "2026-02-03T18:57:19.801Z", "usageCount": 1, "maxUsageCount": 50, "errorScore": 0 }, { - "id": "session_oFBMQoKZBM", + "id": "session_brbza8jFOb", "cookieJar": { "version": "tough-cookie@6.0.0", "storeType": "MemoryCookieStore", @@ -206,28 +206,28 @@ "cookies": [ { "key": "8a164f127e89bfa6ad5b54e0547581b9", - "value": "4oab9p5g0sjkpg0bad165hvss5", + "value": "7klasgpre1eajidc52coun8cg4", "domain": "www.schleicher-gruppe.de", "path": "/", "secure": true, "httpOnly": true, "hostOnly": true, - "creation": "2026-02-03T18:37:59.725Z", - "lastAccessed": "2026-02-03T18:37:59.725Z" + "creation": "2026-02-03T18:57:22.108Z", + "lastAccessed": "2026-02-03T18:57:22.108Z" } ] }, "userData": {}, "maxErrorScore": 3, "errorScoreDecrement": 0.5, - "expiresAt": "2026-02-03T19:27:57.848Z", - "createdAt": "2026-02-03T18:37:57.848Z", + "expiresAt": "2026-02-03T19:47:19.802Z", + "createdAt": "2026-02-03T18:57:19.802Z", "usageCount": 1, "maxUsageCount": 50, "errorScore": 0 }, { - "id": "session_7tuITPzdEJ", + "id": "session_J02AHwBimj", "cookieJar": { "version": "tough-cookie@6.0.0", "storeType": "MemoryCookieStore", @@ -238,22 +238,22 @@ "cookies": [ { "key": "8a164f127e89bfa6ad5b54e0547581b9", - "value": "l0i9ipa1hduoo18scpeb489ian", + "value": "24drbvceqmebqusaqpv78ge3mv", "domain": "www.schleicher-gruppe.de", "path": "/", "secure": true, "httpOnly": true, "hostOnly": true, - "creation": "2026-02-03T18:37:59.633Z", - "lastAccessed": "2026-02-03T18:37:59.633Z" + "creation": "2026-02-03T18:57:21.727Z", + "lastAccessed": "2026-02-03T18:57:21.727Z" } ] }, "userData": {}, "maxErrorScore": 3, "errorScoreDecrement": 0.5, - "expiresAt": "2026-02-03T19:27:57.853Z", - "createdAt": "2026-02-03T18:37:57.853Z", + "expiresAt": "2026-02-03T19:47:19.806Z", + "createdAt": "2026-02-03T18:57:19.806Z", "usageCount": 1, "maxUsageCount": 50, "errorScore": 0