feat: Rename "Betrieb & Hosting" to "Sorglos-Paket" and refine PDF pricing module layout and text.
Some checks failed
Build & Deploy Mintel Blog / build-and-deploy (push) Failing after 2m28s

This commit is contained in:
2026-02-05 01:53:12 +01:00
parent 3e10bca3ac
commit c5ad6108ee
9 changed files with 502 additions and 277 deletions

View File

@@ -4,7 +4,7 @@ Preise
Basis Basis
6.000 € einmalig 4.000 € einmalig
Die Grundlage für jede Website: Die Grundlage für jede Website:
• Projekt-Setup & Infrastruktur • Projekt-Setup & Infrastruktur
@@ -23,7 +23,7 @@ Enthält keine Seiten, Inhalte oder Funktionen.
Seite Seite
800 € / Seite 600 € / Seite
Individuell gestaltete Seite Individuell gestaltete Seite
mit Layout, Struktur, Textaufteilung, responsivem Design. mit Layout, Struktur, Textaufteilung, responsivem Design.
@@ -32,7 +32,7 @@ mit Layout, Struktur, Textaufteilung, responsivem Design.
Feature (System) Feature (System)
2.000 € / Feature 1.500 € / Feature
Ein in sich geschlossenes System mit Datenstruktur, Darstellung und Pflegefähigkeit. Ein in sich geschlossenes System mit Datenstruktur, Darstellung und Pflegefähigkeit.
@@ -50,7 +50,7 @@ Ein Feature erzeugt ein Datenmodell, Übersichten & Detailseiten.
Funktion (Logik) Funktion (Logik)
1.000 € / Funktion 800 € / Funktion
Funktionen liefern Logik und Interaktion, z. B.: Funktionen liefern Logik und Interaktion, z. B.:
• Kontaktformular • Kontaktformular
@@ -73,7 +73,7 @@ Jede Funktion ist ein klar umrissener Logikbaustein.
Visuelle Inszenierung Visuelle Inszenierung
2.000 € / Abschnitt 1.500 € / Abschnitt
Erweiterte Gestaltung: Erweiterte Gestaltung:
• Hero-Story • Hero-Story
@@ -100,7 +100,7 @@ Dargestellte, interaktive UI-Erlebnisse:
Neuer Datensatz Neuer Datensatz
400 € / Stück 200 € / Stück
Beispiele: Beispiele:
• Produkt • Produkt
@@ -126,7 +126,7 @@ Datensatz anpassen
Hosting & Betrieb Hosting & Betrieb
12 Monate = 1440 € 12 Monate = 3.000 €
Sichert: Sichert:
• Webhosting & Verfügbarkeit • Webhosting & Verfügbarkeit
@@ -195,7 +195,7 @@ Für:
API-Schnittstelle / Daten-Sync API-Schnittstelle / Daten-Sync
1.000 € / Zielsystem 800 € / Zielsystem
Synchronisation zu externem System (Push): Synchronisation zu externem System (Push):
• Produkt-Sync • Produkt-Sync

View File

@@ -7,7 +7,8 @@ import { execSync } from 'node:child_process';
import axios from 'axios'; import axios from 'axios';
import { FileCacheAdapter } from '../src/utils/cache/file-adapter.js'; import { FileCacheAdapter } from '../src/utils/cache/file-adapter.js';
import { initialState } from '../src/logic/pricing/constants.js'; import { initialState, PRICING } from '../src/logic/pricing/constants.js';
import { calculateTotals } from '../src/logic/pricing/calculator.js';
async function main() { async function main() {
const OPENROUTER_KEY = process.env.OPENROUTER_KEY; const OPENROUTER_KEY = process.env.OPENROUTER_KEY;
@@ -19,6 +20,7 @@ async function main() {
let briefing = ''; let briefing = '';
let targetUrl: string | null = null; let targetUrl: string | null = null;
let comments: string | null = null; let comments: string | null = null;
let budget: string | null = null;
let cacheKey: string | null = null; let cacheKey: string | null = null;
let jsonStatePath: string | null = null; let jsonStatePath: string | null = null;
@@ -32,6 +34,8 @@ async function main() {
targetUrl = args[++i]; targetUrl = args[++i];
} else if (arg === '--comments' || arg === '--notes') { } else if (arg === '--comments' || arg === '--notes') {
comments = args[++i]; comments = args[++i];
} else if (arg === '--budget') {
budget = args[++i];
} else if (arg === '--cache-key') { } else if (arg === '--cache-key') {
cacheKey = args[++i]; cacheKey = args[++i];
} else if (arg === '--json') { } else if (arg === '--json') {
@@ -77,7 +81,7 @@ async function main() {
} }
const cache = new FileCacheAdapter({ prefix: 'ai_est_' }); const cache = new FileCacheAdapter({ prefix: 'ai_est_' });
const finalCacheKey = cacheKey || `${briefing}_${targetUrl}_${comments}`; const finalCacheKey = cacheKey || `${briefing}_${targetUrl}_${comments}_${budget}`;
// 1. Crawl if URL provided // 1. Crawl if URL provided
let crawlContext = ''; let crawlContext = '';
@@ -103,6 +107,11 @@ async function main() {
distilledCrawl = await distillCrawlContext(crawlContext, OPENROUTER_KEY); distilledCrawl = await distillCrawlContext(crawlContext, OPENROUTER_KEY);
await cache.set(`distilled_${targetUrl}`, distilledCrawl, 86400); await cache.set(`distilled_${targetUrl}`, distilledCrawl, 86400);
} }
} else if (targetUrl) {
distilledCrawl = `WARNING: The crawl of ${targetUrl} failed (ENOTFOUND or timeout).
The AI must NOT hallucinate details about the current website.
Focus ONLY on the BRIEFING provided. If details are missing, mark them as 'unknown'.`;
console.warn('⚠️ Crawl failed. AI will be notified to avoid hallucinations.');
} }
// 3. AI Prompting // 3. AI Prompting
@@ -124,7 +133,7 @@ async function main() {
console.log('📦 Using cached AI response.'); console.log('📦 Using cached AI response.');
formState = cachedAi; formState = cachedAi;
} else { } else {
const result = await getAiEstimation(briefing, distilledCrawl, comments, OPENROUTER_KEY, principles, techStandards, tone); const result = await getAiEstimation(briefing, distilledCrawl, comments, budget, OPENROUTER_KEY, principles, techStandards, tone);
formState = result.state; formState = result.state;
usage = result.usage; usage = result.usage;
await cache.set(finalCacheKey, formState); await cache.set(finalCacheKey, formState);
@@ -251,14 +260,24 @@ async function performCrawl(url: string): Promise<string> {
const cleanJson = (str: string) => { const cleanJson = (str: string) => {
// Remove markdown code blocks if present // Remove markdown code blocks if present
let cleaned = str.replace(/```json\n?|```/g, '').trim(); let cleaned = str.replace(/```json\n?|```/g, '').trim();
// Remove potential control characters that break JSON.parse // Remove potential control characters that break JSON.parse
cleaned = cleaned.replace(/[\u0000-\u001F\u007F-\u009F]/g, " "); // We keep \n \r \t for now as they might be escaped or need handling
// Remove trailing commas before closing braces/brackets cleaned = cleaned.replace(/[\u0000-\u0009\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, " ");
// Specific fix for Gemini: raw newlines inside strings
// This is tricky. We'll try to escape newlines that are NOT followed by a quote and colon (property start)
// or a closing brace/bracket. This is heuristic.
// A better way is to replace all raw newlines that are preceded by a non-backslash with \n
// but only if they are inside double quotes.
// Simplest robust approach: Remove trailing commas and hope response_format does its job.
cleaned = cleaned.replace(/,\s*([\]}])/g, '$1'); cleaned = cleaned.replace(/,\s*([\]}])/g, '$1');
return cleaned; return cleaned;
}; };
const getAiEstimation = async (briefing: string, distilledCrawl: string, comments: string | null, apiKey: string, principles: string, techStandards: string, tone: string) => { const getAiEstimation = async (briefing: string, distilledCrawl: string, comments: string | null, budget: string | null, apiKey: string, principles: string, techStandards: string, tone: string) => {
let usage = { prompt: 0, completion: 0, cost: 0 }; let usage = { prompt: 0, completion: 0, cost: 0 };
const addUsage = (data: any) => { const addUsage = (data: any) => {
if (data?.usage) { if (data?.usage) {
@@ -281,90 +300,150 @@ const getAiEstimation = async (briefing: string, distilledCrawl: string, comment
You are a precision sensor. Analyze the BRIEFING and extract ONLY the raw facts. You are a precision sensor. Analyze the BRIEFING and extract ONLY the raw facts.
Tone: Literal, non-interpretive. Tone: Literal, non-interpretive.
Output language: GERMAN (Strict). Output language: GERMAN (Strict).
Output format: ROOT LEVEL JSON (No wrapper keys like '0' or 'data').
### MISSION: ### MISSION:
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. 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: ### PRICING REFERENCE (FOR CALCULATION):
- Extract **companyName**: The full legal and brand name (e.g., "E-TIB GmbH"). Use signatures and crawl data. - Base Project (Infrastructure + 12 Months Hosting): 5.440 € (MANDATORY START)
- 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. - Additional Pages: 600 € / stk
- Extract **existingWebsite**: The primary URL mentioned in the briefing or signature (e.g., "www.e-tib.com"). - System-Modules (Features): 1.500 € / stk
- Extract **websiteTopic**: A short descriptor of the CORE BUSINESS (e.g., "Kabeltiefbau"). MAX 3 WORDS. - Logic-Functions: 800 € / stk
- **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". - API Integrations: 800 € / stk
- **CRITICAL LOGIC**: If a URL is mentioned, isRelaunch MUST be TRUE. - CMS Setup: 1.500 € (optional)
- For all textual values (deadline, websiteTopic, targetAudience etc.): USE GERMAN. - Visual Staging/Interactions: 1.500 € - 2.000 €
- **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).
- **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): ${budget ? `### BUDGET LOGIC (ULTRA-STRICT):
- **selectedPages**: [Home, About, Services, Contact, Landing, Legal] 1. **Mental Calculation**: Start with 7.000 €. Add items based on the reference above.
- **features**: [blog_news, products, jobs, refs, events] 2. **Hard Ceiling**: If total > ${budget}, you MUST discard lower priority items.
- **functions**: [search, filter, pdf, forms, members, calendar, multilang, chat] 3. **Priority**: High-End Design and Core Pages > Features.
- **apiSystems**: [crm_erp, payment, marketing, ecommerce, maps, social, analytics] 4. **Restriction**: For ${budget}, do NOT exceed 2 features and 4 extra pages.
- **assets**: [existing_website, logo, styleguide, content_concept, media, icons, illustrations, fonts] 5. THE TOTAL COST CALCULATED BY THESE RULES MUST BE <= ${budget}.
6. Do NOT mention the budget in any string fields.` : ''}
### OUTPUT FORMAT (Strict JSON): - ** features **: Items from the FEATURE_REFERENCE.
- ** ABSOLUTE CONSERVATIVE RULE **: Only use features if the briefing implies *dynamic complexity* (CMS, filtering, search, database).
- Simple keywords like 'Karriere', 'Referenzen', 'Messen' or lists of items MUST be treated as simple pages. Add them to 'otherPages' instead.
- If in doubt, categorizing as a PAGE is the mandatory default.
- ** otherPages **: Any specific pages mentioned (e.g. 'Historie', 'Team', 'Partner') that are not in the standard list. Use this for static lists of jobs or references too.
- ** companyName **: The full legal and brand name (e.g., "E-TIB GmbH"). Use signatures and crawl data.
- ** personName **: The name of the primary human contact (e.g., "Danny Joseph"). ** CRITICAL **: Check email signatures and "Mit freundlichen Grüßen" blocks.
- ** email **: The email address of the contact person if found in the briefing / signature.
- ** existingWebsite **: The primary URL mentioned in the briefing or signature (e.g., "www.e-tib.com").
- ** 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.
- ** CRITICAL LOGIC **: If a URL is mentioned, isRelaunch MUST be TRUE.
- For all textual values: USE GERMAN.
- ** multilang **: ONLY if the briefing mentions multiple target languages.
- ** maps **: If "Google Maps" or location maps are mentioned or implicit.
- ** CRITICAL **: Do NOT include "social" in apiSystems unless the user explicitly wants to SYNC / POST content.
### 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 - ROOT LEVEL):
{ {
"companyName": string, "companyName": string,
"companyAddress": string, "companyAddress": string,
"personName": string, "personName": string,
"existingWebsite": string, "email": string,
"websiteTopic": string, "existingWebsite": string,
"isRelaunch": boolean, "websiteTopic": string,
"selectedPages": string[], "isRelaunch": boolean,
"features": string[], "selectedPages": string[],
"functions": string[], "features": string[],
"apiSystems": string[], "functions": string[],
"assets": string[], "apiSystems": string[],
"deadline": string (GERMAN), "assets": string[],
"targetAudience": "B2B" | "B2C" | "Internal" | string (GERMAN), "deadline": string(GERMAN),
"expectedAdjustments": "low" | "medium" | "high" | string (GERMAN), "targetAudience": "B2B" | "B2C" | "Internal" | string(GERMAN),
"employeeCount": "ca. 10+" | "ca. 50+" | "ca. 100+" | "ca. 250+" | "ca. 500+" | "ca. 1000+" "expectedAdjustments": "low" | "medium" | "high" | string(GERMAN),
"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}`; const pass1UserPrompt = `BRIEFING(TRUTH SOURCE): \n${briefing} \n\nCOMMENTS: \n${comments} \n\nDISTILLED_CRAWL(CONTEXT ONLY): \n${distilledCrawl} `;
const p1Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { const p1Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview', model: 'google/gemini-3-flash-preview',
messages: [{ role: 'system', content: pass1SystemPrompt }, { role: 'user', content: pass1UserPrompt }], messages: [{ role: 'system', content: pass1SystemPrompt }, { role: 'user', content: pass1UserPrompt }],
response_format: { type: 'json_object' } response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); }, { headers: { 'Authorization': `Bearer ${apiKey} `, 'Content-Type': 'application/json' } });
if (!p1Resp.data.choices?.[0]?.message?.content) { if (!p1Resp.data.choices?.[0]?.message?.content) {
console.error('❌ Pass 1 failed. Response:', JSON.stringify(p1Resp.data, null, 2)); console.error('❌ Pass 1 failed. Response:', JSON.stringify(p1Resp.data, null, 2));
throw new Error('Pass 1: No content in response'); throw new Error('Pass 1: No content in response');
} }
const facts = JSON.parse(cleanJson(p1Resp.data.choices[0].message.content)); const facts = JSON.parse(cleanJson(p1Resp.data.choices[0].message.content));
// 1.5. PASS 1.5: The Feature Auditor (Skeptical Review)
console.log(' ↳ Pass 1.5: The Feature Auditor (Skeptical Review)...');
const pass15SystemPrompt = `
You are a "Strict Cost Controller". Your mission is to prevent over-billing.
Review the extracted FEATURES and the BRIEFING.
### RULE OF THUMB:
- A "Feature" (1.500 €) is ONLY justified for complex, dynamic systems (logic, database, CMS-driven management, advanced filtering).
- Simple lists, information sections, or static descriptions (e.g., "Messen", "Team", "Historie", "Jobs" as mere text) are ALWAYS "Pages" (600 €).
- If the briefing doesn't explicitly mention "Management System", "Filterable Database", or "Client Login", it is likely a PAGE.
### MISSION:
Analyze each feature in the list. Decide if it should stay a "Feature" or be downgraded to the "otherPages" array.
### OUTPUT FORMAT:
Return only the corrected 'features' and 'otherPages' arrays.
{
"features": string[],
"otherPages": string[]
}
`;
const p15Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview',
messages: [
{ role: 'system', content: pass15SystemPrompt },
{ role: 'user', content: `EXTRACTED_FEATURES: ${JSON.stringify(facts.features)} \nBRIEFING: \n${briefing}` }
],
response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } });
addUsage(p15Resp.data);
const auditResult = JSON.parse(cleanJson(p15Resp.data.choices[0].message.content));
// Apply Audit: Downgrade features to otherPages
facts.features = auditResult.features || [];
facts.otherPages = Array.from(new Set([...(facts.otherPages || []), ...(auditResult.otherPages || [])]));
// 2. PASS 2: Feature Deep-Dive // 2. PASS 2: Feature Deep-Dive
console.log(' ↳ Pass 2: Feature Deep-Dive...'); console.log(' ↳ Pass 2: Feature Deep-Dive...');
const pass2SystemPrompt = ` const pass2SystemPrompt = `
You are a detail-oriented Solution Architect. 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. For EVERY item selected in Pass 1(pages, features, functions, apiSystems), write a specific justification and technical scope.
### RULES: ### RULES:
1. **CONCRETE & SPECIFIC**: Do NOT say "Implementation of X". Say "Displaying X with Y filters". 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. 2. ** JUSTIFICATION (CRITICAL) **: For every entry in 'featureDetails', explicitly explain WHY this is a complex system (1.500 €) and not just a static page (600 €). If it's just a list of items, it's a PAGE.
3. **ABSOLUTE RULE**: EVERYTHING MUST BE GERMAN. 3. ** NO EFFECTS **: Do not mention "fade-ins", "animations" or "visual styling". Focus on FUNCTION.
4. **TRANSPARENCY**: Explain exactly what the USER gets. 4. ** ABSOLUTE RULE **: EVERYTHING MUST BE GERMAN.
5. **API NOTE**: For 'media' or 'video', explicitly state "Upload & Integration" (NO STREAMING). 5. ** TRANSPARENCY **: Explain exactly what the USER gets.
6. ** API NOTE **: For 'media' or 'video', explicitly state "Upload & Integration" (NO STREAMING).
### INPUT (from Pass 1): ### INPUT (from Pass 1):
${JSON.stringify(facts, null, 2)} ${JSON.stringify(facts, null, 2)}
### OUTPUT FORMAT (Strict JSON): ### OUTPUT FORMAT(Strict JSON):
{ {
"pageDetails": { "Home": string, ... }, "pageDetails": { "Home": string, ... },
"featureDetails": { "blog_news": string, ... }, "featureDetails": { "blog_news": string, ... },
"functionDetails": { "search": string, ... }, "functionDetails": { "search": string, ... },
"apiDetails": { "crm_erp": string, ... } "apiDetails": { "crm_erp": string, ... }
} }
`; `;
const p2Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { const p2Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview', model: 'google/gemini-3-flash-preview',
messages: [{ role: 'system', content: pass2SystemPrompt }, { role: 'user', content: briefing }], messages: [{ role: 'system', content: pass2SystemPrompt }, { role: 'user', content: briefing }],
response_format: { type: 'json_object' } response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); }, { headers: { 'Authorization': `Bearer ${apiKey} `, 'Content-Type': 'application/json' } });
addUsage(p2Resp.data); addUsage(p2Resp.data);
if (!p2Resp.data.choices?.[0]?.message?.content) { if (!p2Resp.data.choices?.[0]?.message?.content) {
console.error('❌ Pass 2 failed. Response:', JSON.stringify(p2Resp.data, null, 2)); console.error('❌ Pass 2 failed. Response:', JSON.stringify(p2Resp.data, null, 2));
@@ -375,39 +454,39 @@ ${JSON.stringify(facts, null, 2)}
// 3. PASS 3: Strategic Content (Bespoke Strategy) // 3. PASS 3: Strategic Content (Bespoke Strategy)
console.log(' ↳ Pass 3: Strategic Content (Bespoke Strategy)...'); console.log(' ↳ Pass 3: Strategic Content (Bespoke Strategy)...');
const pass3SystemPrompt = ` const pass3SystemPrompt = `
You are a high-end Digital Architect. Your goal is to make the CUSTOMER feel 100% understood. 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. Analyze the BRIEFING and the EXISTING WEBSITE context.
### TONE & COMMUNICATION PRINCIPLES (STRICT): ### TONE & COMMUNICATION PRINCIPLES(STRICT):
${tone} ${tone}
### OBJECTIVE: ### OBJECTIVE:
3. **briefingSummary**: Ein sachlicher, tiefgehender Überblick der Unternehmenslage. 3. ** briefingSummary **: Ein sachlicher, tiefgehender Überblick der Unternehmenslage.
- **STIL**: Keine Ich-Form. Keine Marketing-Floskeln. Nutze präzise Fachbegriffe. Sei prägnant und effizient (ca. 70% der vorherigen Länge). - ** STIL **: Keine Ich - Form.Keine Marketing - Floskeln.Nutze präzise Fachbegriffe.Sei prägnant und effizient(ca. 70 % der vorherigen Länge).
- **FORM**: EXAKT ZWEI ABSÄTZE. Insgesamt ca. 6 Sätze. - ** FORM **: EXAKT ZWEI ABSÄTZE.Insgesamt ca. 6 Sätze.
- **INHALT**: Welcher technologische Sprung ist notwendig? Was ist der Status Quo? (Bezug zur URL/Briefing). - ** INHALT **: Welcher technologische Sprung ist notwendig ? Was ist der Status Quo ? (Bezug zur URL / Briefing).
- **ABSOLUTE REGEL**: Keine Halluzinationen über fehlende Präsenzen bei Relaunches. - ** ABSOLUTE REGEL **: Keine Halluzinationen über fehlende Präsenzen bei Relaunches.
- **DATENSCHUTZ**: KEINERLEI namentliche Nennungen von Personen (z. B. "Danny Joseph") in diesen Texten. - ** DATENSCHUTZ **: KEINERLEI namentliche Nennungen von Personen(z.B. "Danny Joseph") in diesen Texten.
4. **designVision**: Ein abstraktes, strategisches Konzept. 4. ** designVision **: Ein abstraktes, strategisches Konzept.
- **STIL**: Rein konzeptionell. Keine Umsetzungsschritte. Keinerlei "To-dos". Keine Ich-Form. Sei prägnant. - ** STIL **: Rein konzeptionell.Keine Umsetzungsschritte.Keinerlei "To-dos".Keine Ich - Form.Sei prägnant.
- **FORM**: EXAKT ZWEI ABSÄTZE. Insgesamt ca. 4 Sätze. - ** FORM **: EXAKT ZWEI ABSÄTZE.Insgesamt ca. 4 Sätze.
- **DATENSCHUTZ**: KEINERLEI namentliche Nennungen von Personen in diesen Texten. - ** DATENSCHUTZ **: KEINERLEI namentliche Nennungen von Personen in diesen Texten.
- **FOKUS**: Welche strategische Wirkung soll erzielt werden? (Z. B. "Industrielle Souveränität"). - ** FOKUS **: Welche strategische Wirkung soll erzielt werden ? (Z.B. "Industrielle Souveränität").
### OUTPUT FORMAT (Strict JSON): ### OUTPUT FORMAT(Strict JSON):
{ {
"briefingSummary": string, "briefingSummary": string,
"designVision": string "designVision": string
} }
`; `;
const p3Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { const p3Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview', model: 'google/gemini-3-flash-preview',
messages: [ messages: [
{ role: 'system', content: pass3SystemPrompt }, { 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)}` } { 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' } response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); }, { headers: { 'Authorization': `Bearer ${apiKey} `, 'Content-Type': 'application/json' } });
addUsage(p3Resp.data); addUsage(p3Resp.data);
if (!p3Resp.data.choices?.[0]?.message?.content) { if (!p3Resp.data.choices?.[0]?.message?.content) {
console.error('❌ Pass 3 failed. Response:', JSON.stringify(p3Resp.data, null, 2)); console.error('❌ Pass 3 failed. Response:', JSON.stringify(p3Resp.data, null, 2));
@@ -418,29 +497,30 @@ ${tone}
// 4. PASS 4: Information Architecture (Sitemap) // 4. PASS 4: Information Architecture (Sitemap)
console.log(' ↳ Pass 4: Information Architecture...'); console.log(' ↳ Pass 4: Information Architecture...');
const pass4SystemPrompt = ` const pass4SystemPrompt = `
You are a Senior UX Architect. Design a hierarchical sitemap following the 'Industrial Logic' principle. You are a Senior UX Architect.Design a hierarchical sitemap following the 'Industrial Logic' principle.
EVERYTHING MUST BE IN GERMAN. EVERYTHING MUST BE IN GERMAN.
### SITEMAP RULES: ### SITEMAP RULES:
1. **HIERARCHY**: Build a logical tree. Group by category (e.g., "Kern-Präsenz", "Lösungen", "Vertrauen", "Rechtliches"). 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). 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. 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". 4. ** LANGUAGE **: STRICT GERMAN TITLES.Do NOT use "Home", "About", "Services".Use "Startseite", "Über uns", "Leistungen".
5. ** NO IMPLEMENTATION NOTES **: Do NOT add implementation details in parentheses to titles (e.g. NO "Startseite (Hero-Video)", NO "About (Timeline)"). Keep titles clean and abstract.
### DATA CONTEXT: ### DATA CONTEXT:
${JSON.stringify({ facts, strategy }, null, 2)} ${JSON.stringify({ facts, strategy }, null, 2)}
### OUTPUT FORMAT (Strict JSON): ### OUTPUT FORMAT(Strict JSON):
{ {
"websiteTopic": string, "websiteTopic": string,
"sitemap": [ { "category": string, "pages": [ { "title": string, "desc": string } ] } ] "sitemap": [{ "category": string, "pages": [{ "title": string, "desc": string }] }]
} }
`; `;
const p4Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { const p4Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview', model: 'google/gemini-3-flash-preview',
messages: [{ role: 'system', content: pass4SystemPrompt }, { role: 'user', content: `BRIEFING (TRUTH SOURCE):\n${briefing}\n\nDISTILLED_CRAWL (CONTEXT):\n${distilledCrawl}` }], messages: [{ role: 'system', content: pass4SystemPrompt }, { role: 'user', content: `BRIEFING(TRUTH SOURCE): \n${briefing} \n\nDISTILLED_CRAWL(CONTEXT): \n${distilledCrawl} ` }],
response_format: { type: 'json_object' } response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); }, { headers: { 'Authorization': `Bearer ${apiKey} `, 'Content-Type': 'application/json' } });
addUsage(p4Resp.data); addUsage(p4Resp.data);
if (!p4Resp.data.choices?.[0]?.message?.content) { if (!p4Resp.data.choices?.[0]?.message?.content) {
console.error('❌ Pass 4 failed. Response:', JSON.stringify(p4Resp.data, null, 2)); console.error('❌ Pass 4 failed. Response:', JSON.stringify(p4Resp.data, null, 2));
@@ -450,57 +530,75 @@ ${JSON.stringify({ facts, strategy }, null, 2)}
// 5. PASS 5: Position Synthesis & Pricing Transparency // 5. PASS 5: Position Synthesis & Pricing Transparency
console.log(' ↳ Pass 5: Position Synthesis...'); console.log(' ↳ Pass 5: Position Synthesis...');
// Determine which positions are actually relevant to avoid hallucinations
const requiredPositions = [
"1. Das technische Fundament",
facts.selectedPages.length + facts.otherPages.length > 0 ? "2. Individuelle Seiten" : null,
facts.features.length > 0 ? "3. System-Module (Features)" : null,
facts.functions.length > 0 ? "4. Logik-Funktionen" : null,
facts.apiSystems.length > 0 ? "5. Schnittstellen (API)" : null,
facts.cmsSetup ? "6. Inhaltsverwaltung (CMS)" : null,
"7. Inszenierung & Interaktion", // Always include for high-end strategy
facts.multilang ? "8. Mehrsprachigkeit" : null,
"9. Inhaltliche Initial-Pflege",
"10. Sorglos-Paket (Betrieb & Pflege)"
].filter(Boolean);
const pass5SystemPrompt = ` const pass5SystemPrompt = `
You are a Senior Solution Architect. Your goal is ABSOLUTE TRANSPARENCY and professionalism. You are a Senior Solution Architect. Your goal is ABSOLUTE TRANSPARENCY and professionalism.
Each position in the quote must be perfectly justified and detailed using an objective, technical tone. Each position in the quote must be perfectly justified and detailed using an objective, technical tone.
### POSITION TITLES (STRICT - MUST MATCH EXACTLY): ### REQUIRED POSITION TITLES (STRICT - ONLY DESCRIBE THESE):
"1. Das technische Fundament", "2. Individuelle Seiten", "3. System-Module (Features)", "4. Logik-Funktionen", "5. Schnittstellen (API)", "6. Inhaltsverwaltung (CMS)", "7. Inszenierung & Interaktion", "8. Mehrsprachigkeit", "9. Inhaltliche Initial-Pflege", "10. Laufender Betrieb & Hosting". ${requiredPositions.map(p => `"${p}"`).join(", ")}
### MAPPING RULES (STRICT): ### MAPPING RULES (STRICT):
- **1. Das technische Fundament**: Infrastructure, Hosting setup, SEO-Basics, Analytics, Environments. - ** 1. Das technische Fundament **: Infrastructure, Hosting setup, SEO-Basics, Analytics, Environments.
- **2. Individuelle Seiten**: Layout/structure for specific pages (Home, About, etc.). - ** 2. Individuelle Seiten **: Layout / structure for specific pages. ** RULE **: If quantity is high (e.g. > 10), lead with "Umsetzung von [QTY] individuellen Einzelseiten...".
- **3. System-Module (Features)**: Functional systems like Blog, News, Products, Jobs, References. - ** 3. System-Module (Features) **: Functional systems like Blog, News, Products, Jobs, References. ** RULE **: Describe exactly 1 thing if qty is 1. If qty is 0, DO NOT DESCRIBE THIS.
- **4. Logik-Funktionen**: Logic modules like Search, Filter, Forms, PDF-Export. - ** 4. Logik-Funktionen **: Logic modules like Search, Filter, Forms, PDF-Export.
- **5. Schnittstellen (API)**: Data Syncs with CRM, ERP, Payment systems. - ** 5. Schnittstellen (API) **: Data Syncs with CRM, ERP, Payment systems.
- **6. Inhaltsverwaltung (CMS)**: Setup and mapping for CMS. - ** 6. Inhaltsverwaltung (CMS) **: Setup and mapping for CMS.
- **7. Inszenierung & Interaktion**: Hero-stories, visual effects, configurators. - ** 7. Inszenierung & Interaktion **: Hero-stories, visual effects, configurators.
- **8. Mehrsprachigkeit**: Architecture scaling for multiple languages. - ** 8. Mehrsprachigkeit **: Architecture scaling for multiple languages.
- **9. Inhaltliche Initial-Pflege**: Manual data entry/cleanup. - ** 9. Inhaltliche Initial-Pflege **: Manual data entry / cleanup.
- **10. Laufender Betrieb & Hosting**: Ongoing maintenance, updates, 24/7 monitoring. - ** 10. Sorglos-Paket (Betrieb & Pflege) **: ** RULE **: Describe as "1 Jahr Sicherung des technischen Betriebs, Instandhaltung, Sicherheits-Updates und Inhalts-Aktualisierungen gemäß AGB Punkt 7a."
### RULES FOR positionDescriptions (STRICT): ### RULES FOR positionDescriptions(STRICT):
1. **ABSOLUTE RULE: NO FIRST PERSON**: NEVER use "Ich", "Mein", "Wir" or "Unser". Lead with nouns or passive verbs. 1. ** ABSOLUTE RULE: NO FIRST PERSON **: NEVER use "Ich", "Mein", "Wir" or "Unser". Lead with nouns or passive verbs.
2. **PROFESSIONAL TONE**: Use "Erstellung von...", "Anbindung der...", "Implementierung technischer...", "Bereitstellung von...". 2. ** QUANTITY PARITY (ULTRA-STRICT) **: The description MUST list EXACTLY the number of items matching the 'qty' for that position. If qty is 3, describe exactly 3 items. If qty is 1, describe exactly 1 item. Do NOT "stuff" additional features into one description.
3. **CONCISE & ITEM-BASED**: Use technical, high-density sentences. Name specific industry terms from context. 3. ** LOGIC GUARD (CMS) **: If 'cmsSetup' is false in the DATA CONTEXT, you MUST NOT mention "CMS", "Modul", "Management System" or "Inhaltsverwaltung". Use "Statische Seite" or "Darstellung".
4. **ITEMIZED SYNTHESIS**: Mention EVERY component selected in Pass 1. 4. ** STATIC vs DYNAMIC **: If no complex logic was extracted in Pass 2 for a feature, describe it as a technical layout/page, not as a system.
5. **HARD SPECIFICS**: If the briefing mentions "Glasfaser-Trassen" or "Schwerlast-Logistik", IT MUST BE IN THE DESCRIPTION. 5. ** PROFESSIONAL TONE **: Use "Erstellung von...", "Anbindung der...", "Implementierung technischer...", "Bereitstellung von...".
6. **INDUSTRIAL AMBITION**: Describe it as a high-end technical solution. Avoid "schöne Website" or marketing fluff. 6. ** CONCISE & ITEM-BASED **: Use technical, high-density sentences. Name specific industry terms from context.
7. **PAGES**: For "2. Individuelle Seiten", list the pages (e.g., "Startseite (Hero-Video), Leistungen, Kontakt"). 7. ** ITEMIZED SYNTHESIS **: Mention EVERY component selected in Pass 1.
8. **LOGIC**: Describe the ACTUAL logic (e.g., "Volltextsuche mit Auto-Complete", not "eine Suche"). 8. ** HARD SPECIFICS **: If the briefing mentions "Glasfaser-Trassen" or "Schwerlast-Logistik", IT MUST BE IN THE DESCRIPTION.
9. **KEYS**: Return EXACTLY the keys defined in "POSITION TITLES". 9. ** INDUSTRIAL AMBITION **: Describe it as a high-end technical solution. Avoid "schöne Website" or marketing fluff.
10. **NO AGB**: NEVER mention "AGB" or "Geschäftsbedingungen". 10. ** PAGES **: For "2. Individuelle Seiten", list the pages. ** ABSOLUTE RULE **: Do NOT add implementation details or technical notes in parentheses (e.g. NO "(Matrix-Struktur)", NO "(Timeline-Modul)"). Use clean titles like "Startseite, Über uns, Leistungen".
11. ** LOGIC **: Describe the ACTUAL logic (e.g., "Volltextsuche mit Auto-Complete", not "eine Suche").
12. ** KEYS **: Return EXACTLY the keys defined in "POSITION TITLES".
13. ** NO AGB **: NEVER mention "AGB" or "Geschäftsbedingungen".
### EXAMPLES (PASSIVE & TECHNICAL): ### EXAMPLES(PASSIVE & TECHNICAL):
- **GOOD**: "Konfiguration der CMS-Infrastruktur zur unabhängigen Verwaltung von Produkt-Katalogen und News-Beiträgen." - ** GOOD **: "Konfiguration der CMS-Infrastruktur zur unabhängigen Verwaltung von Produkt-Katalogen und News-Beiträgen."
- **GOOD**: "Implementierung einer Volltextsuche inkl. Kategorisierungs-Logik für effizientes Auffinden von Projektreferenzen." - ** GOOD **: "Implementierung einer Volltextsuche inkl. Kategorisierungs-Logik für effizientes Auffinden von Projektreferenzen."
- **GOOD**: "Native API-Anbindung an das ERP-System zur Echtzeit-Synchronisation von Bestandsdaten." - ** GOOD **: "Native API-Anbindung an das ERP-System zur Echtzeit-Synchronisation von Bestandsdaten."
- **BAD**: "Ich richte dir das CMS ein." - ** BAD **: "Ich richte dir das CMS ein."
- **BAD**: "Ich programmiere eine tolle Suche für deine Seite." - ** BAD **: "Ich programmiere eine tolle Suche für deine Seite."
### DATA CONTEXT: ### DATA CONTEXT:
${JSON.stringify({ facts, details, strategy, ia }, null, 2)} ${JSON.stringify({ facts, details, strategy, ia }, null, 2)}
### OUTPUT FORMAT (Strict JSON): ### OUTPUT FORMAT(Strict JSON):
{ {
"positionDescriptions": { "1. Das technische Fundament": string, ... } "positionDescriptions": { "1. Das technische Fundament": string, ... }
} }
`; `;
const p5Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { const p5Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview', model: 'google/gemini-3-flash-preview',
messages: [{ role: 'system', content: pass5SystemPrompt }, { role: 'user', content: briefing }], messages: [{ role: 'system', content: pass5SystemPrompt }, { role: 'user', content: briefing }],
response_format: { type: 'json_object' } response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); }, { headers: { 'Authorization': `Bearer ${apiKey} `, 'Content-Type': 'application/json' } });
addUsage(p5Resp.data); addUsage(p5Resp.data);
if (!p5Resp.data.choices?.[0]?.message?.content) { if (!p5Resp.data.choices?.[0]?.message?.content) {
console.error('❌ Pass 5 failed. Response:', JSON.stringify(p5Resp.data, null, 2)); console.error('❌ Pass 5 failed. Response:', JSON.stringify(p5Resp.data, null, 2));
@@ -511,31 +609,32 @@ ${JSON.stringify({ facts, details, strategy, ia }, null, 2)}
// 6. PASS 6: The Industrial Critic // 6. PASS 6: The Industrial Critic
console.log(' ↳ Pass 6: The Industrial Critic (Quality Gate)...'); console.log(' ↳ Pass 6: The Industrial Critic (Quality Gate)...');
const pass6SystemPrompt = ` const pass6SystemPrompt = `
You are the "Industrial Critic". Your goal is to catch quality regressions and ensure the document is bespoke, technical, and professional. You are the "Industrial Critic".Your goal is to catch quality regressions and ensure the document is bespoke, technical, and professional.
Analyze the CURRENT_STATE against the BRIEFING_TRUTH. Analyze the CURRENT_STATE against the BRIEFING_TRUTH.
### CRITICAL ERROR CHECKLIST (FAIL IF FOUND): ### CRITICAL ERROR CHECKLIST(FAIL IF FOUND):
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. 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. - ** 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. 2. ** Logic Conflict **: FAIL if isRelaunch is true but briefingSummary claims no website exists.
- FAIL if the description in positionDescriptions mentions more items than extracted in facts. - FAIL if the description in positionDescriptions mentions more items than extracted in facts.
3. **Implementation Fluff**: FAIL if tech-stack details are mentioned (React, etc.). Focus on Concept & Result. 3. ** Implementation Fluff **: FAIL if tech - stack details are mentioned(React, etc.).Focus on Concept & Result.
4. **Genericism Check (CRITICAL)**: FAIL if any text sounds like it could apply to ANY company. It MUST mention specific industry details (e.g., "Kabeltiefbau", "Infrastruktur-Zentrum") from the Briefing or Crawl. 4. ** Genericism Check(CRITICAL) **: FAIL if any text sounds like it could apply to ANY company.It MUST mention specific industry details(e.g., "Kabeltiefbau", "Infrastruktur-Zentrum") from the Briefing or Crawl.
6. **Namen-Verbot (STRICT)**: FAIL if any personal names (e.g. "Danny Joseph", "Joseph", etc.) appear in 'briefingSummary' or 'designVision'. Use abstract terms like "Unternehmensführung" or "Management" if necessary. 6. ** Namen-Verbot (STRICT) **: FAIL if any personal names (e.g. "Danny Joseph", "Joseph", etc.) appear in 'briefingSummary' or 'designVision'. Use abstract terms like "Unternehmensführung" or "Management" if necessary.
7. **AGB BAN**: FAIL if "Allgemeine Geschäftsbedingungen" or "AGB" appear anywhere. 7. ** LOGIC GUARD (CMS) **: If 'cmsSetup' is false in the DATA CONTEXT, FAIL if any 'positionDescriptions' or 'briefingSummary' mentions "CMS", "Content Management System" or "Inhaltsverwaltung".
8. **Length Check**: Briefing (ca. 6 Sätze) und Vision (ca. 4 Sätze). Kürze Texte, die zu ausschweifend sind, auf das Wesentliche. 8. ** AGB BAN **: FAIL if "Allgemeine Geschäftsbedingungen" or "AGB" appear anywhere.
9. ** Length Check **: Briefing (ca. 6 Sätze) und Vision (ca. 4 Sätze). Kürze Texte, die zu ausschweifend sind, auf das Wesentliche.
### MISSION: ### MISSION:
Return updated fields ONLY. Specifically focus on hardening 'positionDescriptions', 'sitemap', 'briefingSummary', and 'designVision'. Return updated fields ONLY.Specifically focus on hardening 'positionDescriptions', 'sitemap', 'briefingSummary', and 'designVision'.
### DATA CONTEXT: ### DATA CONTEXT:
${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)} ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)}
`; `;
const p6Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', { const p6Resp = await axios.post('https://openrouter.ai/api/v1/chat/completions', {
model: 'google/gemini-3-flash-preview', model: 'google/gemini-3-flash-preview',
messages: [{ role: 'system', content: pass6SystemPrompt }, { role: 'user', content: `BRIEFING_TRUTH:\n${briefing}` }], messages: [{ role: 'system', content: pass6SystemPrompt }, { role: 'user', content: `BRIEFING_TRUTH: \n${briefing} ` }],
response_format: { type: 'json_object' } response_format: { type: 'json_object' }
}, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); }, { headers: { 'Authorization': `Bearer ${apiKey} `, 'Content-Type': 'application/json' } });
addUsage(p6Resp.data); addUsage(p6Resp.data);
if (!p6Resp.data.choices?.[0]?.message?.content) { if (!p6Resp.data.choices?.[0]?.message?.content) {
console.error('❌ Pass 6 failed. Response:', JSON.stringify(p6Resp.data, null, 2)); console.error('❌ Pass 6 failed. Response:', JSON.stringify(p6Resp.data, null, 2));
@@ -548,12 +647,10 @@ ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)}
let result = { ...state }; let result = { ...state };
const unwrap = (obj: any): any => { const unwrap = (obj: any): any => {
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj; if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj;
// Always unwrap "0" if it exists, regardless of other keys (AI often nests)
if (obj["0"]) return unwrap(obj["0"]); if (obj["0"]) return unwrap(obj["0"]);
if (obj.state) return unwrap(obj.state); if (obj.state && Object.keys(obj).length === 1) return unwrap(obj.state);
if (obj.facts) return unwrap(obj.facts); if (obj.facts && Object.keys(obj).length === 1) return unwrap(obj.facts);
if (obj.strategy) return unwrap(obj.strategy);
if (obj.ia) return unwrap(obj.ia);
if (obj.positionsData) return unwrap(obj.positionsData);
return obj; return obj;
}; };
@@ -576,6 +673,10 @@ ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)}
finalState.statusQuo = facts.isRelaunch ? 'Relaunch' : 'Neuentwicklung'; finalState.statusQuo = facts.isRelaunch ? 'Relaunch' : 'Neuentwicklung';
// Recipient Mapping
if (finalState.personName) finalState.name = finalState.personName;
if (finalState.email) finalState.email = finalState.email;
// Normalization Layer: Map hallucinated German keys back to internal keys // Normalization Layer: Map hallucinated German keys back to internal keys
const normalizationMap: Record<string, string> = { const normalizationMap: Record<string, string> = {
"Briefing-Zusammenfassung": "briefingSummary", "Briefing-Zusammenfassung": "briefingSummary",
@@ -596,6 +697,27 @@ ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)}
} }
}); });
// Final Logic Guard: Strictly strip CMS from ALL descriptions and fields if not enabled
if (!finalState.cmsSetup) {
const stripCMS = (obj: any): any => {
if (typeof obj === 'string') {
return obj.replace(/CMS|Content-Management-System|Inhaltsverwaltung/gi, 'Plattform-Struktur');
}
if (Array.isArray(obj)) {
return obj.map(stripCMS);
}
if (obj !== null && typeof obj === 'object') {
const newObj: any = {};
Object.entries(obj).forEach(([k, v]) => {
newObj[k] = stripCMS(v);
});
return newObj;
}
return obj;
};
finalState = stripCMS(finalState);
}
// Sitemap Normalization (German keys to internal) // Sitemap Normalization (German keys to internal)
if (Array.isArray(finalState.sitemap)) { if (Array.isArray(finalState.sitemap)) {
finalState.sitemap = finalState.sitemap.map((cat: any) => ({ finalState.sitemap = finalState.sitemap.map((cat: any) => ({
@@ -607,14 +729,41 @@ ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)}
})); }));
} }
// Position Descriptions Normalization // Position Descriptions Normalization (Strict Title Mapping + Index-based Fallback)
if (finalState.positionDescriptions) { if (finalState.positionDescriptions) {
const normalized: Record<string, string> = {}; const normalized: Record<string, string> = {};
Object.entries(finalState.positionDescriptions).forEach(([key, value]) => { const rawPositions = finalState.positionDescriptions;
const normalizedKey = key === 'titel' || key === 'Title' ? 'title' : key;
// 1. Initial cleanup
Object.entries(rawPositions).forEach(([key, value]) => {
const normalizedValue = typeof value === 'object' ? (value as any).beschreibung || (value as any).description || JSON.stringify(value) : value; const normalizedValue = typeof value === 'object' ? (value as any).beschreibung || (value as any).description || JSON.stringify(value) : value;
normalized[normalizedKey] = normalizedValue as string; normalized[key] = normalizedValue as string;
}); });
// 2. Index-based matching (Map "10. Foo" to "10. Bar")
const standardTitles = [
"1. Das technische Fundament",
"2. Individuelle Seiten",
"3. System-Module (Features)",
"4. Logik-Funktionen",
"5. Schnittstellen (API)",
"6. Inhaltsverwaltung (CMS)",
"7. Inszenierung & Interaktion",
"8. Mehrsprachigkeit",
"9. Inhaltliche Initial-Pflege",
"10. Sorglos-Paket (Betrieb & Pflege)"
];
standardTitles.forEach(std => {
const prefix = std.split('.')[0] + '.'; // e.g., "10."
// Find any key in the AI output that starts with this number
const matchingKey = Object.keys(normalized).find(k => k.trim().startsWith(prefix));
if (matchingKey && matchingKey !== std) {
normalized[std] = normalized[matchingKey];
// Keep the old key too just in case, but prioritize the standard one
}
});
finalState.positionDescriptions = normalized; finalState.positionDescriptions = normalized;
} }
@@ -639,6 +788,69 @@ ${JSON.stringify({ facts, strategy, ia, positionsData }, null, 2)}
} }
} }
// Final Post-Reflection Budget Sync (Hard Pruning if still over)
if (budget) {
const targetValue = parseInt(budget.replace(/[^0-9]/g, ''));
if (!isNaN(targetValue)) {
console.log(`⚖️ Final Budget Audit(${targetValue} € target)...`);
let currentTotals = calculateTotals(finalState, PRICING);
// Step-by-step pruning if too expensive
if (currentTotals.totalPrice > targetValue) {
console.log(`⚠️ Budget exceeded(${currentTotals.totalPrice} €).Pruning scope to fit ${targetValue} €...`);
// 1. Remove optional "other" stuff
finalState.otherFeatures = [];
finalState.otherFunctions = [];
finalState.otherTech = [];
// 2. Remove non-critical functions
const funcPriority = ['search', 'filter', 'calendar', 'multilang'];
for (const f of funcPriority) {
if (currentTotals.totalPrice <= targetValue) break;
if (finalState.functions.includes(f)) {
finalState.functions = finalState.functions.filter((x: string) => x !== f);
currentTotals = calculateTotals(finalState, PRICING);
}
}
// 3. Remove least critical features if still over
const featurePriority = ['events', 'blog_news', 'products'];
for (const p of featurePriority) {
if (currentTotals.totalPrice <= targetValue) break;
if (finalState.features.includes(p)) {
finalState.features = finalState.features.filter((f: string) => f !== p);
currentTotals = calculateTotals(finalState, PRICING);
}
}
// 4. Reduce page count (Selected Pages AND Sitemap)
while (currentTotals.totalPrice > targetValue && (finalState.selectedPages.length > 4 || currentTotals.totalPagesCount > 5)) {
if (finalState.selectedPages.length > 4) {
finalState.selectedPages.pop();
}
// Prune Sitemap to match
if (finalState.sitemap && Array.isArray(finalState.sitemap)) {
const lastCat = finalState.sitemap[finalState.sitemap.length - 1];
if (lastCat && lastCat.pages && lastCat.pages.length > 0) {
lastCat.pages.pop();
if (lastCat.pages.length === 0) finalState.sitemap.pop();
}
}
currentTotals = calculateTotals(finalState, PRICING);
}
// 5. Final fallback: Remove second feature if still over
if (currentTotals.totalPrice > targetValue && finalState.features.length > 1) {
finalState.features.pop();
currentTotals = calculateTotals(finalState, PRICING);
}
}
console.log(`✅ Final budget audit complete: ${currentTotals.totalPrice}`);
}
}
return { state: finalState, usage }; return { state: finalState, usage };
} }

View File

@@ -104,7 +104,7 @@ export function PriceCalculation({
</div> </div>
<div className="pt-4 border-t border-slate-200 space-y-4"> <div className="pt-4 border-t border-slate-200 space-y-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-slate-600 font-medium text-sm">Betrieb & Hosting</span> <span className="text-slate-600 font-medium text-sm">Sorglos-Paket</span>
<span className="text-base font-bold text-slate-900">{monthlyPrice.toLocaleString()} / Monat</span> <span className="text-base font-bold text-slate-900">{monthlyPrice.toLocaleString()} / Monat</span>
</div> </div>
</div> </div>

View File

@@ -54,9 +54,9 @@ export const AboutModule = () => (
</PDFView> </PDFView>
<PDFView style={{ marginTop: 32, paddingVertical: 16, borderTopWidth: 1, borderTopColor: COLORS.GRID }}> <PDFView style={{ marginTop: 32, paddingVertical: 16, borderTopWidth: 1, borderTopColor: COLORS.GRID }}>
<PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 4 }]}>Qualitäts-Zusage</PDFText> <PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 4 }]}>Infrastruktur & Souveränität</PDFText>
<PDFText style={styles.industrialText}> <PDFText style={styles.industrialText}>
Es werden keine instabilen Prototypen geliefert, sondern produktionsreife Software, die skalierbar bleibt und mit dem Unternehmen wächst. Direkt. Sauber. Ohne Ballast. Es wird keine instabile Prototyp-Software geliefert, sondern produktionsreife Systeme, die technisch skalierbar bleiben. Die Codebasis folgt modernen Standards bei wachsenden Ansprüchen oder dem Wechsel zu einer Agentur kann der Quellcode jederzeit nahtlos übernommen und weitergeführt werden.
</PDFText> </PDFText>
</PDFView> </PDFView>
</PDFView> </PDFView>
@@ -65,8 +65,8 @@ export const AboutModule = () => (
export const CrossSellModule = ({ state }: any) => { export const CrossSellModule = ({ state }: any) => {
const isWebsite = state.projectType === 'website'; const isWebsite = state.projectType === 'website';
const title = isWebsite ? "Routine-Automatisierung" : "Websites & Ökosysteme"; const title = isWebsite ? "Weitere Potenziale" : "Websites & Ökosysteme";
const subtitle = isWebsite ? "Maßgeschneiderte Software zur Prozessoptimierung" : "Technische Infrastruktur ohne Kompromisse"; const subtitle = isWebsite ? "Automatisierung und Prozessoptimierung" : "Technische Infrastruktur ohne Kompromisse";
return ( return (
<> <>
@@ -77,7 +77,7 @@ export const CrossSellModule = ({ state }: any) => {
{isWebsite ? ( {isWebsite ? (
<> <>
<PDFView style={[styles.industrialCol, { marginRight: '8%' }]}> <PDFView style={[styles.industrialCol, { marginRight: '8%' }]}>
<PDFText style={styles.industrialTextLead}>Manuelle Alltags-Aufgaben sind teuer und fehleranfällig. In einer separaten Beauftragung können maßgeschneiderte Systeme entwickelt werden, die Routine-Prozesse automatisiert im Hintergrund verarbeiten.</PDFText> <PDFText style={styles.industrialTextLead}>Über die klassische Webpräsenz hinaus werden maßgeschneiderte Lösungen zur Automatisierung von Routine-Prozessen angeboten. Dies ermöglicht eine signifikante Effizienzsteigerung im Tagesgeschäft.</PDFText>
<PDFText style={[styles.industrialText, { fontWeight: 'bold' }]}>Keine Abos. Keine komplexen neuen Systeme. Gezielte Zeitersparnis.</PDFText> <PDFText style={[styles.industrialText, { fontWeight: 'bold' }]}>Keine Abos. Keine komplexen neuen Systeme. Gezielte Zeitersparnis.</PDFText>
<PDFView style={{ marginTop: 24, padding: 16, backgroundColor: '#f8fafc', borderLeftWidth: 2, borderLeftColor: COLORS.GRID }}> <PDFView style={{ marginTop: 24, padding: 16, backgroundColor: '#f8fafc', borderLeftWidth: 2, borderLeftColor: COLORS.GRID }}>
<PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 4 }]}>Individuelle Analyse</PDFText> <PDFText style={[styles.industrialText, { fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 4 }]}>Individuelle Analyse</PDFText>

View File

@@ -5,11 +5,11 @@ import { View as PDFView, Text as PDFText, StyleSheet, Image as PDFImage } from
import { DocumentTitle, Divider, COLORS, FONT_SIZES } from '../SharedUI'; import { DocumentTitle, Divider, COLORS, FONT_SIZES } from '../SharedUI';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
section: { marginBottom: 24 }, section: { marginBottom: 16 },
pricingGrid: { marginTop: 24 }, pricingGrid: { marginTop: 12 },
pricingRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: COLORS.DIVIDER, paddingVertical: 10, alignItems: 'flex-start' }, pricingRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: COLORS.DIVIDER, paddingVertical: 12, alignItems: 'flex-start' },
pricingTitle: { width: '30%', fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.CHARCOAL }, pricingTitle: { width: '30%', fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.CHARCOAL, paddingRight: 15 },
pricingDesc: { width: '55%', fontSize: FONT_SIZES.SUB, color: COLORS.TEXT_DIM, lineHeight: 1.4 }, pricingDesc: { width: '55%', fontSize: FONT_SIZES.SUB, color: COLORS.TEXT_DIM, lineHeight: 1.5, paddingRight: 10 },
pricingTag: { width: '15%', fontSize: FONT_SIZES.BODY, fontWeight: 'bold', textAlign: 'right', color: COLORS.CHARCOAL }, 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 }, configLabel: { fontSize: FONT_SIZES.BLUEPRINT, color: COLORS.TEXT_LIGHT, textTransform: 'uppercase', marginBottom: 8 },
}); });
@@ -18,12 +18,11 @@ export const techPageModule = ({ techDetails, headerIcon }: any) => (
<> <>
<DocumentTitle title="Technische Umsetzung" /> <DocumentTitle title="Technische Umsetzung" />
<PDFView style={styles.section}> <PDFView style={styles.section}>
<PDFText style={{ fontSize: FONT_SIZES.SUB, color: COLORS.TEXT_DIM, lineHeight: 1.6, marginBottom: 16 }}>Entwicklung von Websites als moderne, performante Websysteme nach industriellen Standards.</PDFText>
<PDFView style={styles.pricingGrid}> <PDFView style={styles.pricingGrid}>
{techDetails?.map((item: any, i: number) => ( {techDetails?.map((item: any, i: number) => (
<PDFView key={i} style={styles.pricingRow}> <PDFView key={i} style={styles.pricingRow}>
<PDFText style={[styles.pricingTitle, { width: '35%' }]}>{item.t}</PDFText> <PDFText style={[styles.pricingTitle, { width: '30%' }]}>{item.t}</PDFText>
<PDFText style={[styles.pricingDesc, { width: '65%' }]}>{item.d}</PDFText> <PDFText style={[styles.pricingDesc, { width: '70%', paddingRight: 0 }]}>{item.d}</PDFText>
</PDFView> </PDFView>
))} ))}
</PDFView> </PDFView>
@@ -34,62 +33,57 @@ export const techPageModule = ({ techDetails, headerIcon }: any) => (
export const TransparenzModule = ({ pricing }: any) => ( export const TransparenzModule = ({ pricing }: any) => (
<> <>
<DocumentTitle title="Preis-Transparenz & Modell" /> <DocumentTitle title="Preis-Transparenz & Modell" />
<PDFView style={styles.section}>
<PDFText style={{ fontSize: FONT_SIZES.BODY + 1, fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 8 }}>Festpreise statt Stundenabrechnung</PDFText>
<PDFText style={{ fontSize: FONT_SIZES.SUB, color: COLORS.TEXT_DIM, lineHeight: 1.6, marginBottom: 12 }}>Maximale Planungssicherheit durch ein modulares Festpreis-System. Die Abrechnung erfolgt nach Ergebnissen statt nach reinem Zeitaufwand. Versteckte Kosten sind durch die modulare Struktur ausgeschlossen.</PDFText>
<Divider style={{ marginTop: 12 }} />
</PDFView>
<PDFView style={styles.section}> <PDFView style={styles.section}>
<PDFView style={styles.pricingGrid}> <PDFView style={styles.pricingGrid}>
<PDFView style={styles.pricingRow}> <PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>1. Das technische Fundament</PDFText> <PDFText style={styles.pricingTitle}>1. Fundament</PDFText>
<PDFText style={styles.pricingDesc}>Einrichtung der technischen Infrastruktur, Hosting-Bereitstellung, SEO-Basics sowie Bereitstellung von Test-, Staging- und produktiven Live-Umgebungen.</PDFText> <PDFText style={styles.pricingDesc}>Setup, Infrastruktur, Hosting, SEO-Basics, Staging & Live-Umgebungen.</PDFText>
<PDFText style={styles.pricingTag}>{pricing.BASE_WEBSITE?.toLocaleString('de-DE')} </PDFText> <PDFText style={styles.pricingTag}>{pricing.BASE_WEBSITE?.toLocaleString('de-DE')} </PDFText>
</PDFView> </PDFView>
<PDFView style={styles.pricingRow}> <PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>2. Individuelle Seiten</PDFText> <PDFText style={styles.pricingTitle}>2. Seiten</PDFText>
<PDFText style={styles.pricingDesc}>Entwicklung individueller Layouts und Strukturen pro Seite. Optimierung für alle Endgeräte (Responsive Design) und Browser-Kompatibilität.</PDFText> <PDFText style={styles.pricingDesc}>Layout & Umsetzung individueller Seiten. Responsive Design / Cross-Browser.</PDFText>
<PDFText style={styles.pricingTag}>{pricing.PAGE?.toLocaleString('de-DE')} / Stk</PDFText> <PDFText style={styles.pricingTag}>{pricing.PAGE?.toLocaleString('de-DE')} / Stk</PDFText>
</PDFView> </PDFView>
<PDFView style={styles.pricingRow}> <PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>3. System-Module (Features)</PDFText> <PDFText style={styles.pricingTitle}>3. Features</PDFText>
<PDFText style={styles.pricingDesc}>Implementierung abgeschlossener technischer Systeme (z. B. Blog, News, Produkte). Definition notwendiger Datenfelder und Pflege-Oberflächen.</PDFText> <PDFText style={styles.pricingDesc}>Abgeschlossene Systeme (z. B. Blog, Jobs, Produkte) inkl. Datenstruktur.</PDFText>
<PDFText style={styles.pricingTag}>{pricing.FEATURE?.toLocaleString('de-DE')} / Stk</PDFText> <PDFText style={styles.pricingTag}>{pricing.FEATURE?.toLocaleString('de-DE')} / Stk</PDFText>
</PDFView> </PDFView>
<PDFView style={styles.pricingRow}> <PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>4. Logik-Funktionen</PDFText> <PDFText style={styles.pricingTitle}>4. Funktionen</PDFText>
<PDFText style={styles.pricingDesc}>Programmierung technischer Logik-Einheiten wie Filter, Suchen oder Kontakt-Schnittstellen zur fehlerfreien Datenverarbeitung.</PDFText> <PDFText style={styles.pricingDesc}>Logik-Einheiten wie Filter, Suchen oder Kontakt-Schnittstellen.</PDFText>
<PDFText style={styles.pricingTag}>{pricing.FUNCTION?.toLocaleString('de-DE')} / Stk</PDFText> <PDFText style={styles.pricingTag}>{pricing.FUNCTION?.toLocaleString('de-DE')} / Stk</PDFText>
</PDFView> </PDFView>
<PDFView style={styles.pricingRow}> <PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>5. Schnittstellen (API)</PDFText> <PDFText style={styles.pricingTitle}>5. Schnittstellen</PDFText>
<PDFText style={styles.pricingDesc}>Anbindung externer Drittsysteme (CRM, ERP, Payment-Provider) zur automatisierten Datensynchronisation und Prozessoptimierung.</PDFText> <PDFText style={styles.pricingDesc}>Anbindung externer Systeme (CRM, ERP, Payment) zur Synchronisation.</PDFText>
<PDFText style={styles.pricingTag}>ab {pricing.API_INTEGRATION?.toLocaleString('de-DE')} / Stk</PDFText> <PDFText style={styles.pricingTag}>ab {pricing.API_INTEGRATION?.toLocaleString('de-DE')} / Stk</PDFText>
</PDFView> </PDFView>
<PDFView style={styles.pricingRow}> <PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>6. Inhaltsverwaltung (CMS)</PDFText> <PDFText style={styles.pricingTitle}>6. CMS Setup</PDFText>
<PDFText style={styles.pricingDesc}>Bereitstellung und Konfiguration eines Content Management Systems (CMS) inkl. technischer Anbindung aller Module zur unabhängigen Datenpflege.</PDFText> <PDFText style={styles.pricingDesc}>Konfiguration Headless CMS zur unabhängigen Datenpflege aller Module.</PDFText>
<PDFText style={styles.pricingTag}>{pricing.CMS_SETUP?.toLocaleString('de-DE')} </PDFText> <PDFText style={pricing.CMS_SETUP ? styles.pricingTag : [styles.pricingTag, { color: COLORS.TEXT_LIGHT }]}>{pricing.CMS_SETUP?.toLocaleString('de-DE')} </PDFText>
</PDFView> </PDFView>
<PDFView style={styles.pricingRow}> <PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>7. Inszenierung & Interaktion</PDFText> <PDFText style={styles.pricingTitle}>7. Inszenierung</PDFText>
<PDFText style={styles.pricingDesc}>Umsetzung komplexer Interaktions-Mechanismen, Konfiguratoren oder aufwendiger visueller Storytelling-Elemente zur Steigerung der Conversion.</PDFText> <PDFText style={styles.pricingDesc}>Interaktions-Mechanismen, Konfiguratoren oder visuelles Storytelling.</PDFText>
<PDFText style={styles.pricingTag}>ab {pricing.VISUAL_STAGING?.toLocaleString('de-DE')} </PDFText> <PDFText style={styles.pricingTag}>ab {pricing.VISUAL_STAGING?.toLocaleString('de-DE')} </PDFText>
</PDFView> </PDFView>
<PDFView style={styles.pricingRow}> <PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>8. Mehrsprachigkeit</PDFText> <PDFText style={styles.pricingTitle}>8. Sprachen</PDFText>
<PDFText style={styles.pricingDesc}>Skalierung der System-Architektur auf zusätzliche Sprachen. Inklusive struktureller Anpassungen und Logik für Sprachumschaltung.</PDFText> <PDFText style={styles.pricingDesc}>Skalierung der System-Architektur auf zusätzliche Sprachversionen.</PDFText>
<PDFText style={styles.pricingTag}>+20% / Sprache</PDFText> <PDFText style={styles.pricingTag}>+20% / Sprache</PDFText>
</PDFView> </PDFView>
<PDFView style={styles.pricingRow}> <PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>9. Inhaltliche Initial-Pflege</PDFText> <PDFText style={styles.pricingTitle}>9. Initial-Pflege</PDFText>
<PDFText style={styles.pricingDesc}>Manuelle Übernahme und Aufbereitung von Datensätzen in das Zielsystem zur Sicherstellung einer initialen Inhaltsabdeckung.</PDFText> <PDFText style={styles.pricingDesc}>Manuelle Aufbereitung & Übernahme von Datensätzen in das Zielsystem.</PDFText>
<PDFText style={styles.pricingTag}>{pricing.NEW_DATASET?.toLocaleString('de-DE')} / Stk</PDFText> <PDFText style={styles.pricingTag}>{pricing.NEW_DATASET?.toLocaleString('de-DE')} / Stk</PDFText>
</PDFView> </PDFView>
<PDFView style={styles.pricingRow}> <PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>10. Laufender Betrieb & Hosting</PDFText> <PDFText style={styles.pricingTitle}>10. Sorglos-Paket</PDFText>
<PDFText style={styles.pricingDesc}>Laufender Betrieb, Hosting, Sicherheits-Updates sowie monatliche Backups und Monitoring zur Sicherstellung der Systemverfügbarkeit.</PDFText> <PDFText style={styles.pricingDesc}>Betrieb, Hosting, Updates & Monitoring gemäß AGB Punkt 7a.</PDFText>
<PDFText style={styles.pricingTag}>{pricing.HOSTING_MONTHLY?.toLocaleString('de-DE')} / Monat</PDFText> <PDFText style={styles.pricingTag}>Inklusive 1 Jahr</PDFText>
</PDFView> </PDFView>
</PDFView> </PDFView>
</PDFView> </PDFView>
@@ -99,11 +93,11 @@ export const TransparenzModule = ({ pricing }: any) => (
export const PrinciplesModule = ({ principles }: any) => ( export const PrinciplesModule = ({ principles }: any) => (
<> <>
<DocumentTitle title="Prinzipien & Standards" /> <DocumentTitle title="Prinzipien & Standards" />
<PDFView style={styles.pricingGrid}> <PDFView style={[styles.pricingGrid, { marginTop: 8 }]}>
{principles?.map((item: any, i: number) => ( {principles?.map((item: any, i: number) => (
<PDFView key={i} style={styles.pricingRow}> <PDFView key={i} style={styles.pricingRow}>
<PDFText style={[styles.pricingTitle, { width: '35%' }]}>{item.t}</PDFText> <PDFText style={[styles.pricingTitle, { width: '30%' }]}>{item.t}</PDFText>
<PDFText style={[styles.pricingDesc, { width: '65%' }]}>{item.d}</PDFText> <PDFText style={[styles.pricingDesc, { width: '70%', paddingRight: 0 }]}>{item.d}</PDFText>
</PDFView> </PDFView>
))} ))}
</PDFView> </PDFView>

View File

@@ -183,8 +183,8 @@ export function calculatePositions(state: FormState, pricing: any): Position[] {
const monthlyRate = pricing.HOSTING_MONTHLY + (state.storageExpansion * pricing.STORAGE_EXPANSION_MONTHLY); const monthlyRate = pricing.HOSTING_MONTHLY + (state.storageExpansion * pricing.STORAGE_EXPANSION_MONTHLY);
positions.push({ positions.push({
pos: pos++, pos: pos++,
title: '10. Laufender Betrieb & Hosting', title: '10. Sorglos-Paket (Betrieb & Pflege)',
desc: `Bereitstellung der Infrastruktur, technische Instandhaltung, Sicherheits-Updates und Backup-Management gemäß AGB Punkt 7a. Inklusive 12 Monate Service.`, desc: `1 Jahr Sicherung des technischen Betriebs, Instandhaltung, Sicherheits-Updates und Inhalts-Aktualisierungen gemäß AGB Punkt 7a.`,
qty: 1, qty: 1,
price: monthlyRate * 12 price: monthlyRate * 12
}); });

View File

@@ -1,16 +1,16 @@
import { FormState } from './types'; import { FormState } from './types';
export const PRICING = { export const PRICING = {
BASE_WEBSITE: 6000, BASE_WEBSITE: 4000,
PAGE: 800, PAGE: 600,
FEATURE: 2000, FEATURE: 1500,
FUNCTION: 1000, FUNCTION: 800,
NEW_DATASET: 400, NEW_DATASET: 200,
HOSTING_MONTHLY: 120, HOSTING_MONTHLY: 250,
STORAGE_EXPANSION_MONTHLY: 10, STORAGE_EXPANSION_MONTHLY: 10,
CMS_SETUP: 1500, CMS_SETUP: 1500,
CMS_CONNECTION_PER_FEATURE: 800, CMS_CONNECTION_PER_FEATURE: 800,
API_INTEGRATION: 1000, API_INTEGRATION: 800,
APP_HOURLY: 120, APP_HOURLY: 120,
VISUAL_STAGING: 2000, VISUAL_STAGING: 2000,
COMPLEX_INTERACTION: 1500, COMPLEX_INTERACTION: 1500,

View File

@@ -1,68 +1,27 @@
{ {
"requestsFinished": 0, "requestsFinished": 7,
"requestsFailed": 1, "requestsFailed": 0,
"requestsRetries": 1, "requestsRetries": 0,
"requestsFailedPerMinute": 279, "requestsFailedPerMinute": 0,
"requestsFinishedPerMinute": 0, "requestsFinishedPerMinute": 596,
"requestMinDurationMillis": null, "requestMinDurationMillis": 72,
"requestMaxDurationMillis": 0, "requestMaxDurationMillis": 503,
"requestTotalFailedDurationMillis": 2, "requestTotalFailedDurationMillis": 0,
"requestTotalFinishedDurationMillis": 0, "requestTotalFinishedDurationMillis": 1167,
"crawlerStartedAt": "2026-02-04T17:39:09.193Z", "crawlerStartedAt": "2026-02-05T00:44:22.537Z",
"crawlerFinishedAt": "2026-02-04T17:39:09.396Z", "crawlerFinishedAt": "2026-02-05T00:44:23.230Z",
"statsPersistedAt": "2026-02-04T17:39:09.396Z", "statsPersistedAt": "2026-02-05T00:44:23.230Z",
"crawlerRuntimeMillis": 215, "crawlerRuntimeMillis": 705,
"crawlerLastStartTimestamp": 1770226749181, "crawlerLastStartTimestamp": 1770252262525,
"requestRetryHistogram": [ "requestRetryHistogram": [
null, 7
null,
null,
1
], ],
"statsId": 0, "statsId": 0,
"requestAvgFailedDurationMillis": 2, "requestAvgFailedDurationMillis": null,
"requestAvgFinishedDurationMillis": null, "requestAvgFinishedDurationMillis": 167,
"requestTotalDurationMillis": 2, "requestTotalDurationMillis": 1167,
"requestsTotal": 1, "requestsTotal": 7,
"requestsWithStatusCode": {}, "requestsWithStatusCode": {},
"errors": { "errors": {},
"file:///Users/marcmintel/Projects/mintel.me/node_modules/got/dist/source/core/index.js:198:21": { "retryErrors": {}
"ENOTFOUND": {
"RequestError": {
"getaddrinfo ENOTFOUND etib-et.com": {
"count": 1
}
}
}
},
"node:dns:120:26": {
"ENOTFOUND": {
"Error": {
"getaddrinfo ENOTFOUND etib-et.com": {
"count": 1
}
}
}
}
},
"retryErrors": {
"file:///Users/marcmintel/Projects/mintel.me/node_modules/got/dist/source/core/index.js:198:21": {
"ENOTFOUND": {
"RequestError": {
"getaddrinfo ENOTFOUND etib-et.com": {
"count": 3
}
}
}
},
"node:dns:120:26": {
"ENOTFOUND": {
"Error": {
"getaddrinfo ENOTFOUND etib-et.com": {
"count": 3
}
}
}
}
}
} }

View File

@@ -1,9 +1,9 @@
{ {
"usableSessionsCount": 4, "usableSessionsCount": 7,
"retiredSessionsCount": 0, "retiredSessionsCount": 0,
"sessions": [ "sessions": [
{ {
"id": "session_qS9LzMPYLU", "id": "session_MKcXTj8jBY",
"cookieJar": { "cookieJar": {
"version": "tough-cookie@6.0.0", "version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore", "storeType": "MemoryCookieStore",
@@ -16,14 +16,14 @@
"userData": {}, "userData": {},
"maxErrorScore": 3, "maxErrorScore": 3,
"errorScoreDecrement": 0.5, "errorScoreDecrement": 0.5,
"expiresAt": "2026-02-04T18:29:09.266Z", "expiresAt": "2026-02-05T01:34:22.574Z",
"createdAt": "2026-02-04T17:39:09.266Z", "createdAt": "2026-02-05T00:44:22.574Z",
"usageCount": 1, "usageCount": 1,
"maxUsageCount": 50, "maxUsageCount": 50,
"errorScore": 1 "errorScore": 0
}, },
{ {
"id": "session_rQXoQ3YoSI", "id": "session_CzBvH8k5d6",
"cookieJar": { "cookieJar": {
"version": "tough-cookie@6.0.0", "version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore", "storeType": "MemoryCookieStore",
@@ -36,14 +36,14 @@
"userData": {}, "userData": {},
"maxErrorScore": 3, "maxErrorScore": 3,
"errorScoreDecrement": 0.5, "errorScoreDecrement": 0.5,
"expiresAt": "2026-02-04T18:29:09.369Z", "expiresAt": "2026-02-05T01:34:23.083Z",
"createdAt": "2026-02-04T17:39:09.369Z", "createdAt": "2026-02-05T00:44:23.083Z",
"usageCount": 1, "usageCount": 1,
"maxUsageCount": 50, "maxUsageCount": 50,
"errorScore": 1 "errorScore": 0
}, },
{ {
"id": "session_VQKYMg9QOd", "id": "session_6tYd3j1pzA",
"cookieJar": { "cookieJar": {
"version": "tough-cookie@6.0.0", "version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore", "storeType": "MemoryCookieStore",
@@ -56,14 +56,14 @@
"userData": {}, "userData": {},
"maxErrorScore": 3, "maxErrorScore": 3,
"errorScoreDecrement": 0.5, "errorScoreDecrement": 0.5,
"expiresAt": "2026-02-04T18:29:09.376Z", "expiresAt": "2026-02-05T01:34:23.085Z",
"createdAt": "2026-02-04T17:39:09.376Z", "createdAt": "2026-02-05T00:44:23.085Z",
"usageCount": 1, "usageCount": 1,
"maxUsageCount": 50, "maxUsageCount": 50,
"errorScore": 1 "errorScore": 0
}, },
{ {
"id": "session_Jf7MUM5INz", "id": "session_MahMPRKWfS",
"cookieJar": { "cookieJar": {
"version": "tough-cookie@6.0.0", "version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore", "storeType": "MemoryCookieStore",
@@ -76,11 +76,71 @@
"userData": {}, "userData": {},
"maxErrorScore": 3, "maxErrorScore": 3,
"errorScoreDecrement": 0.5, "errorScoreDecrement": 0.5,
"expiresAt": "2026-02-04T18:29:09.380Z", "expiresAt": "2026-02-05T01:34:23.086Z",
"createdAt": "2026-02-04T17:39:09.380Z", "createdAt": "2026-02-05T00:44:23.086Z",
"usageCount": 1, "usageCount": 1,
"maxUsageCount": 50, "maxUsageCount": 50,
"errorScore": 1 "errorScore": 0
},
{
"id": "session_POCFcWGlXP",
"cookieJar": {
"version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore",
"rejectPublicSuffixes": true,
"enableLooseMode": false,
"allowSpecialUseDomain": true,
"prefixSecurity": "silent",
"cookies": []
},
"userData": {},
"maxErrorScore": 3,
"errorScoreDecrement": 0.5,
"expiresAt": "2026-02-05T01:34:23.087Z",
"createdAt": "2026-02-05T00:44:23.087Z",
"usageCount": 1,
"maxUsageCount": 50,
"errorScore": 0
},
{
"id": "session_zra0Syci00",
"cookieJar": {
"version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore",
"rejectPublicSuffixes": true,
"enableLooseMode": false,
"allowSpecialUseDomain": true,
"prefixSecurity": "silent",
"cookies": []
},
"userData": {},
"maxErrorScore": 3,
"errorScoreDecrement": 0.5,
"expiresAt": "2026-02-05T01:34:23.088Z",
"createdAt": "2026-02-05T00:44:23.088Z",
"usageCount": 1,
"maxUsageCount": 50,
"errorScore": 0
},
{
"id": "session_JbCC8UIBHk",
"cookieJar": {
"version": "tough-cookie@6.0.0",
"storeType": "MemoryCookieStore",
"rejectPublicSuffixes": true,
"enableLooseMode": false,
"allowSpecialUseDomain": true,
"prefixSecurity": "silent",
"cookies": []
},
"userData": {},
"maxErrorScore": 3,
"errorScoreDecrement": 0.5,
"expiresAt": "2026-02-05T01:34:23.092Z",
"createdAt": "2026-02-05T00:44:23.092Z",
"usageCount": 1,
"maxUsageCount": 50,
"errorScore": 0
} }
] ]
} }