[split-required] Split website + studio-v2 monoliths (Phase 3 continued)

Website (14 monoliths split):
- compliance/page.tsx (1,519 → 9), docs/audit (1,262 → 20)
- quality (1,231 → 16), alerts (1,203 → 10), docs (1,202 → 11)
- i18n.ts (1,173 → 8 language files)
- unity-bridge (1,094 → 12), backlog (1,087 → 6)
- training (1,066 → 8), rag (1,063 → 8)
- Deleted index_original.ts (4,899 LOC dead backup)

Studio-v2 (5 monoliths split):
- meet/page.tsx (1,481 → 9), messages (1,166 → 9)
- AlertsB2BContext.tsx (1,165 → 5 modules)
- alerts-b2b/page.tsx (1,019 → 6), korrektur/archiv (1,001 → 6)

All existing imports preserved. Zero new TypeScript errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-24 17:52:36 +02:00
parent b681ddb131
commit 0b37c5e692
143 changed files with 15822 additions and 15889 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,149 @@
import type { B2BHit, B2BTopic, ImportanceLabel, DecisionLabel } from './types'
/**
* Process email content into a B2BHit (Manual Import for Testing).
* Pure function — no state mutations.
*/
export function processEmailToHit(
emailContent: string,
tenantId: string,
defaultTopicId: string,
emailSubject?: string
): B2BHit {
// Parse email content to extract useful information
const lines = emailContent.split('\n').filter(l => l.trim())
const title = emailSubject || lines[0]?.slice(0, 100) || 'Manuell eingefuegter Alert'
// Try to extract URLs from content
const urlRegex = /(https?:\/\/[^\s<>"]+)/g
const urls = emailContent.match(urlRegex) || []
const firstUrl = urls[0] || 'https://example.com/manual-import'
// Extract snippet (first meaningful paragraph)
const snippet = lines.slice(0, 3).join(' ').slice(0, 300) || emailContent.slice(0, 300)
// Simulate AI analysis - look for procurement signals
const procurementSignals = ['ausschreibung', 'tender', 'vergabe', 'beschaffung', 'auftrag',
'angebot', 'submission', 'procurement', 'rfp', 'rfq', 'bid']
const productSignals = ['parking', 'parkschein', 'ladesäule', 'ev charging', 'ladestation',
'tankstelle', 'fuel', 'bezahlterminal', 'payment']
const buyerSignals = ['stadt', 'kommune', 'gemeinde', 'city', 'municipality', 'council',
'stadtwerke', 'öffentlich', 'public']
const negativeSignals = ['stellenangebot', 'job', 'karriere', 'news', 'blog', 'press release']
const lowerContent = emailContent.toLowerCase()
const foundProcurement = procurementSignals.filter(s => lowerContent.includes(s))
const foundProducts = productSignals.filter(s => lowerContent.includes(s))
const foundBuyers = buyerSignals.filter(s => lowerContent.includes(s))
const foundNegatives = negativeSignals.filter(s => lowerContent.includes(s))
// Calculate importance score (0-100)
let score = 30 // base score
score += foundProcurement.length * 15
score += foundProducts.length * 10
score += foundBuyers.length * 12
score -= foundNegatives.length * 20
score = Math.max(0, Math.min(100, score))
// Determine importance label
let importanceLabel: ImportanceLabel = 'INFO'
if (score >= 80) importanceLabel = 'KRITISCH'
else if (score >= 65) importanceLabel = 'DRINGEND'
else if (score >= 50) importanceLabel = 'WICHTIG'
else if (score >= 30) importanceLabel = 'PRUEFEN'
// Determine decision label
let decisionLabel: DecisionLabel = 'irrelevant'
let decisionConfidence = 0.5
if (foundNegatives.length > 1) {
decisionLabel = 'irrelevant'
decisionConfidence = 0.8
} else if (foundProcurement.length >= 2 && foundProducts.length >= 1) {
decisionLabel = 'relevant'
decisionConfidence = 0.85
} else if (foundProcurement.length >= 1 || foundProducts.length >= 1) {
decisionLabel = 'needs_review'
decisionConfidence = 0.6
}
// Try to guess buyer and country
let buyerGuess: string | undefined
let countryGuess: string | undefined
const countryPatterns = [
{ pattern: /deutschland|germany|german/i, country: 'DE' },
{ pattern: /österreich|austria|austrian/i, country: 'AT' },
{ pattern: /schweiz|switzerland|swiss/i, country: 'CH' },
{ pattern: /frankreich|france|french/i, country: 'FR' },
{ pattern: /niederlande|netherlands|dutch/i, country: 'NL' },
]
for (const { pattern, country } of countryPatterns) {
if (pattern.test(emailContent)) {
countryGuess = country
break
}
}
// Extract potential buyer name (look for "Stadt X" or "Kommune Y")
const buyerMatch = emailContent.match(/(?:stadt|kommune|gemeinde|city of|municipality of)\s+([A-Za-zäöüß]+)/i)
if (buyerMatch) {
buyerGuess = buyerMatch[0]
}
// Try to find deadline
let deadlineGuess: string | undefined
const dateMatch = emailContent.match(/(\d{1,2})[.\/](\d{1,2})[.\/](\d{2,4})/)
if (dateMatch) {
const day = parseInt(dateMatch[1])
const month = parseInt(dateMatch[2])
const year = dateMatch[3].length === 2 ? 2000 + parseInt(dateMatch[3]) : parseInt(dateMatch[3])
const date = new Date(year, month - 1, day)
if (date > new Date()) {
deadlineGuess = date.toISOString()
}
}
// Create new hit
const newHit: B2BHit = {
id: `manual_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
tenantId,
topicId: defaultTopicId,
sourceType: 'email',
sourceRef: 'manual_import',
originalUrl: firstUrl,
canonicalUrl: firstUrl,
title,
snippet,
fullText: emailContent,
foundAt: new Date(),
language: 'de',
countryGuess,
buyerGuess,
deadlineGuess,
importanceScore: score,
importanceLabel,
decisionLabel,
decisionConfidence,
decisionTrace: {
rulesTriggered: [
...(foundProcurement.length > 0 ? ['procurement_signal_detected'] : []),
...(foundProducts.length > 0 ? ['product_match_found'] : []),
...(foundBuyers.length > 0 ? ['public_buyer_signal'] : []),
...(foundNegatives.length > 0 ? ['negative_signal_detected'] : []),
],
llmUsed: true,
llmConfidence: decisionConfidence,
signals: {
procurementSignalsFound: foundProcurement,
publicBuyerSignalsFound: foundBuyers,
productSignalsFound: foundProducts,
negativesFound: foundNegatives
}
},
isRead: false,
createdAt: new Date()
}
return newHit
}

View File

@@ -0,0 +1,59 @@
import type { ImportanceLabel, DecisionLabel, Package } from './types'
// ============================================
// HELPER FUNCTIONS
// ============================================
export function getImportanceLabelColor(label: ImportanceLabel, isDark: boolean): string {
const colors = {
'KRITISCH': isDark ? 'bg-red-500/20 text-red-300 border-red-500/30' : 'bg-red-100 text-red-700 border-red-200',
'DRINGEND': isDark ? 'bg-orange-500/20 text-orange-300 border-orange-500/30' : 'bg-orange-100 text-orange-700 border-orange-200',
'WICHTIG': isDark ? 'bg-yellow-500/20 text-yellow-300 border-yellow-500/30' : 'bg-yellow-100 text-yellow-700 border-yellow-200',
'PRUEFEN': isDark ? 'bg-blue-500/20 text-blue-300 border-blue-500/30' : 'bg-blue-100 text-blue-700 border-blue-200',
'INFO': isDark ? 'bg-slate-500/20 text-slate-300 border-slate-500/30' : 'bg-slate-100 text-slate-600 border-slate-200',
}
return colors[label]
}
export function getDecisionLabelColor(label: DecisionLabel, isDark: boolean): string {
const colors = {
'relevant': isDark ? 'bg-green-500/20 text-green-300' : 'bg-green-100 text-green-700',
'irrelevant': isDark ? 'bg-slate-500/20 text-slate-400' : 'bg-slate-100 text-slate-500',
'needs_review': isDark ? 'bg-amber-500/20 text-amber-300' : 'bg-amber-100 text-amber-700',
}
return colors[label]
}
export function formatDeadline(dateStr: string | null | undefined): string {
if (!dateStr) return 'Keine Frist'
const date = new Date(dateStr)
const now = new Date()
const diffDays = Math.ceil((date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
if (diffDays < 0) return 'Abgelaufen'
if (diffDays === 0) return 'Heute!'
if (diffDays === 1) return 'Morgen'
if (diffDays <= 7) return `${diffDays} Tage`
if (diffDays <= 30) return `${Math.ceil(diffDays / 7)} Wochen`
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
}
export function getPackageIcon(pkg: Package): string {
const icons = {
'PARKING': '🅿️',
'EV_CHARGING': '⚡',
'FUEL': '⛽',
'TANK_MONITORING': '📊'
}
return icons[pkg]
}
export function getPackageLabel(pkg: Package): string {
const labels = {
'PARKING': 'Parking',
'EV_CHARGING': 'EV Charging',
'FUEL': 'Fuel',
'TANK_MONITORING': 'Tank Monitoring'
}
return labels[pkg]
}

View File

@@ -0,0 +1,427 @@
import type { B2BTemplate, B2BHit, B2BSettings, B2BTenant } from './types'
// ============================================
// HECTRONIC TEMPLATE (Real Example)
// ============================================
export const hectronicTemplate: B2BTemplate = {
templateId: 'tpl_hectronic_public_procurement_v1',
templateName: 'Hectronic Kommunale Ausschreibungen (Parking • EV Charging • Fuel)',
templateDescription: 'Findet weltweit kommunale Ausschreibungen für Parkscheinautomaten/Parkraummanagement, Payment an Ladepunkten und Fuel-Terminals. Reduziert News/Jobs/Zubehör-Rauschen stark.',
targetRoles: ['sales_ops', 'bid_management', 'product_marketing', 'regional_sales'],
industry: 'Payment & Infrastructure',
companyExample: 'Hectronic GmbH',
guidedConfig: {
regionSelector: {
type: 'multi_select',
default: ['EUROPE'],
options: ['EUROPE', 'DACH', 'NORTH_AMERICA', 'MIDDLE_EAST', 'APAC', 'GLOBAL']
},
languageSelector: {
type: 'multi_select',
default: ['de', 'en'],
options: ['de', 'en', 'fr', 'es', 'it', 'nl', 'pl', 'sv', 'da', 'no', 'fi', 'pt']
},
packageSelector: {
type: 'multi_select',
default: ['PARKING', 'EV_CHARGING'],
options: ['PARKING', 'EV_CHARGING', 'FUEL', 'TANK_MONITORING']
},
noiseMode: {
type: 'single_select',
default: 'STRICT',
options: ['STRICT', 'BALANCED', 'BROAD']
}
},
topics: [
{
name: 'Parking Parkscheinautomaten & Parkraummanagement (Kommunen)',
package: 'PARKING',
intent: {
goalStatement: 'Finde kommunale Ausschreibungen/Procurements für Parkscheinautomaten, Pay Stations, Parkraummanagement-Software und zugehörige Services.',
mustHaveAnyN: 2,
mustHave: [
{ type: 'keyword', value: 'parking meter' },
{ type: 'keyword', value: 'pay and display' },
{ type: 'keyword', value: 'parking pay station' },
{ type: 'keyword', value: 'Parkscheinautomat' },
{ type: 'keyword', value: 'Parkautomat' },
{ type: 'keyword', value: 'Parkraumbewirtschaftung' },
{ type: 'keyword', value: 'backoffice' },
{ type: 'keyword', value: 'management software' }
],
publicBuyerSignalsAny: [
'municipality', 'city', 'city council', 'local authority', 'public tender',
'Kommune', 'Stadt', 'Gemeinde', 'Landkreis', 'öffentliche Ausschreibung', 'Vergabe'
],
procurementSignalsAny: [
'tender', 'procurement', 'RFP', 'RFQ', 'invitation to tender', 'contract notice',
'Ausschreibung', 'Vergabe', 'EU-weite Ausschreibung', 'Bekanntmachung'
]
},
filters: {
hardExcludesAny: [
'keyboard', 'mouse', 'headset', 'toner', 'office supplies',
'Tastatur', 'Maus', 'Headset', 'Büromaterial',
'job', 'hiring', 'stellenanzeige', 'bewerbung',
'parking fine', 'parking ticket', 'Strafzettel', 'Knöllchen'
],
softExcludesAny: [
'press release', 'marketing', 'blog', 'opinion',
'pressemitteilung', 'werbung', 'meinung',
'maintenance only', 'consulting only', 'support only'
]
},
decisionPolicy: {
rulesFirst: true,
llmMode: 'GRAYZONE_ONLY',
autoRelevantMinConf: 0.80,
needsReviewRange: [0.50, 0.79],
autoIrrelevantMaxConf: 0.49
},
importanceModelId: 'imp_hectronic_v1',
status: 'active'
},
{
name: 'EV Charging Payment/Terminals in kommunaler Ladeinfrastruktur',
package: 'EV_CHARGING',
intent: {
goalStatement: 'Finde kommunale Ausschreibungen für Ladeinfrastruktur mit Payment-Terminals, Ad-hoc-Kartenzahlung, Backend/Management.',
mustHaveAnyN: 2,
mustHave: [
{ type: 'keyword', value: 'EV charging' },
{ type: 'keyword', value: 'charging station' },
{ type: 'keyword', value: 'charge point' },
{ type: 'keyword', value: 'Ladesäule' },
{ type: 'keyword', value: 'Ladeinfrastruktur' },
{ type: 'keyword', value: 'payment terminal' },
{ type: 'keyword', value: 'card payment' },
{ type: 'keyword', value: 'contactless' },
{ type: 'keyword', value: 'Bezahlsystem' },
{ type: 'keyword', value: 'Kartenzahlung' }
],
publicBuyerSignalsAny: [
'municipality', 'city', 'public works', 'local authority',
'Kommune', 'Stadt', 'Gemeinde', 'Kommunal'
],
procurementSignalsAny: [
'tender', 'procurement', 'RFP', 'RFQ', 'contract notice',
'Ausschreibung', 'Vergabe', 'Bekanntmachung'
]
},
filters: {
hardExcludesAny: [
'stock', 'investor', 'funding round', 'press release',
'Aktie', 'Investor', 'Finanzierung', 'Pressemitteilung',
'job', 'hiring', 'stellenanzeige',
'private home charger', 'wallbox'
],
softExcludesAny: [
'funding program', 'grant', 'Förderprogramm', 'Zuschuss',
'pilot project', 'research project', 'Modellprojekt'
]
},
decisionPolicy: {
rulesFirst: true,
llmMode: 'GRAYZONE_ONLY',
autoRelevantMinConf: 0.80,
needsReviewRange: [0.50, 0.79],
autoIrrelevantMaxConf: 0.49
},
importanceModelId: 'imp_hectronic_v1',
status: 'active'
},
{
name: 'Fuel Fleet/Public Fuel Terminals',
package: 'FUEL',
intent: {
goalStatement: 'Finde Ausschreibungen für (un)bemannte Tankautomaten/Fuel-Terminals, Fuel-Management, Flottenbetankung.',
mustHaveAnyN: 2,
mustHave: [
{ type: 'keyword', value: 'fuel terminal' },
{ type: 'keyword', value: 'fuel management' },
{ type: 'keyword', value: 'unmanned fuel station' },
{ type: 'keyword', value: 'fleet fueling' },
{ type: 'keyword', value: 'Tankautomat' },
{ type: 'keyword', value: 'Tankterminal' },
{ type: 'keyword', value: 'Flottenbetankung' },
{ type: 'keyword', value: 'Betriebstankstelle' }
],
publicBuyerSignalsAny: ['municipality', 'utilities', 'public works', 'Stadtwerke', 'Kommunal'],
procurementSignalsAny: ['tender', 'procurement', 'RFP', 'Ausschreibung', 'Vergabe']
},
filters: {
hardExcludesAny: ['fuel price', 'oil price', 'Spritpreise', 'Ölpreis', 'job', 'hiring', 'stellenanzeige'],
softExcludesAny: ['market news', 'commodity', 'Börse', 'Pressemitteilung']
},
decisionPolicy: {
rulesFirst: true,
llmMode: 'GRAYZONE_ONLY',
autoRelevantMinConf: 0.80,
needsReviewRange: [0.50, 0.79],
autoIrrelevantMaxConf: 0.49
},
importanceModelId: 'imp_hectronic_v1',
status: 'active'
}
],
importanceModel: {
modelId: 'imp_hectronic_v1',
scoreRange: [0, 100],
weights: {
deadlineProximity: 28,
procurementIntentStrength: 22,
publicBuyerStrength: 18,
productMatchStrength: 18,
sourceTrust: 8,
novelty: 6
},
thresholds: {
infoMax: 29,
reviewMin: 30,
importantMin: 50,
urgentMin: 70,
criticalMin: 85
},
topNCaps: {
dailyDigestMax: 10,
weeklyDigestMax: 20,
perTopicDailyMax: 6
}
},
notificationPreset: {
presetId: 'notif_hectronic_digest_v1',
channels: ['email'],
cadence: 'DAILY',
timeLocal: '08:00',
includeStatuses: ['relevant', 'needs_review'],
maxItems: 10
}
}
// ============================================
// MOCK DATA (Demo Hits for Hectronic)
// ============================================
export const mockHectronicHits: B2BHit[] = [
{
id: 'hit-hec-001',
tenantId: 'hectronic-demo',
topicId: 'hec_parking_v1',
sourceType: 'email',
sourceRef: 'ga-msg-001',
originalUrl: 'https://www.evergabe.de/auftraege/parkscheinautomaten-stadt-muenchen',
canonicalUrl: 'https://evergabe.de/auftraege/parkscheinautomaten-muenchen',
title: 'Stadt München: Lieferung und Installation von 150 Parkscheinautomaten',
snippet: 'Die Landeshauptstadt München schreibt die Beschaffung von 150 modernen Parkscheinautomaten inkl. Backoffice-Software für das Stadtgebiet aus. Frist: 15.03.2026.',
foundAt: new Date(Date.now() - 2 * 60 * 60 * 1000),
language: 'de',
countryGuess: 'Germany',
buyerGuess: 'Landeshauptstadt München',
deadlineGuess: '2026-03-15',
importanceScore: 92,
importanceLabel: 'KRITISCH',
decisionLabel: 'relevant',
decisionConfidence: 0.95,
decisionTrace: {
rulesTriggered: ['procurement_signal_match', 'public_buyer_match', 'product_match'],
llmUsed: false,
signals: {
procurementSignalsFound: ['Ausschreibung', 'Beschaffung', 'Vergabe'],
publicBuyerSignalsFound: ['Landeshauptstadt', 'Stadt München'],
productSignalsFound: ['Parkscheinautomaten', 'Backoffice-Software'],
negativesFound: []
}
},
isRead: false,
createdAt: new Date(Date.now() - 2 * 60 * 60 * 1000)
},
{
id: 'hit-hec-002',
tenantId: 'hectronic-demo',
topicId: 'hec_ev_charging_v1',
sourceType: 'rss',
sourceRef: 'rss-item-002',
originalUrl: 'https://ted.europa.eu/notice/2026-12345',
canonicalUrl: 'https://ted.europa.eu/notice/2026-12345',
title: 'EU Tender: Supply of EV Charging Infrastructure with Payment Terminals - City of Amsterdam',
snippet: 'Contract notice for the supply, installation and maintenance of 200 public EV charging points with integrated contactless payment terminals. Deadline: 28.02.2026.',
foundAt: new Date(Date.now() - 5 * 60 * 60 * 1000),
language: 'en',
countryGuess: 'Netherlands',
buyerGuess: 'City of Amsterdam',
deadlineGuess: '2026-02-28',
importanceScore: 88,
importanceLabel: 'KRITISCH',
decisionLabel: 'relevant',
decisionConfidence: 0.92,
decisionTrace: {
rulesTriggered: ['procurement_signal_match', 'public_buyer_match', 'product_match'],
llmUsed: false,
signals: {
procurementSignalsFound: ['Contract notice', 'tender', 'supply'],
publicBuyerSignalsFound: ['City of Amsterdam', 'public'],
productSignalsFound: ['EV charging', 'charging points', 'payment terminals', 'contactless'],
negativesFound: []
}
},
isRead: false,
createdAt: new Date(Date.now() - 5 * 60 * 60 * 1000)
},
{
id: 'hit-hec-003',
tenantId: 'hectronic-demo',
topicId: 'hec_parking_v1',
sourceType: 'email',
sourceRef: 'ga-msg-003',
originalUrl: 'https://vergabe.niedersachsen.de/NetServer/notice/8765432',
canonicalUrl: 'https://vergabe.niedersachsen.de/notice/8765432',
title: 'Gemeinde Seevetal: Erneuerung Parkraumbewirtschaftungssystem',
snippet: 'Öffentliche Ausschreibung zur Erneuerung des Parkraumbewirtschaftungssystems mit 25 Pay-Stations und zentraler Management-Software.',
foundAt: new Date(Date.now() - 8 * 60 * 60 * 1000),
language: 'de',
countryGuess: 'Germany',
buyerGuess: 'Gemeinde Seevetal',
deadlineGuess: '2026-04-01',
importanceScore: 78,
importanceLabel: 'DRINGEND',
decisionLabel: 'relevant',
decisionConfidence: 0.88,
decisionTrace: {
rulesTriggered: ['procurement_signal_match', 'public_buyer_match', 'product_match'],
llmUsed: false,
signals: {
procurementSignalsFound: ['Öffentliche Ausschreibung', 'Erneuerung'],
publicBuyerSignalsFound: ['Gemeinde'],
productSignalsFound: ['Parkraumbewirtschaftungssystem', 'Pay-Stations', 'Management-Software'],
negativesFound: []
}
},
isRead: false,
createdAt: new Date(Date.now() - 8 * 60 * 60 * 1000)
},
{
id: 'hit-hec-004',
tenantId: 'hectronic-demo',
topicId: 'hec_parking_v1',
sourceType: 'email',
sourceRef: 'ga-msg-004',
originalUrl: 'https://news.example.com/parking-industry-trends-2026',
canonicalUrl: 'https://news.example.com/parking-trends-2026',
title: 'Parking Industry Trends 2026: Smart Cities investieren in digitale Lösungen',
snippet: 'Branchenanalyse zeigt wachsende Nachfrage nach vernetzten Parkscheinautomaten. Experten erwarten 15% Marktwachstum.',
foundAt: new Date(Date.now() - 12 * 60 * 60 * 1000),
language: 'de',
countryGuess: 'Germany',
buyerGuess: undefined,
deadlineGuess: undefined,
importanceScore: 15,
importanceLabel: 'INFO',
decisionLabel: 'irrelevant',
decisionConfidence: 0.91,
decisionTrace: {
rulesTriggered: ['soft_exclude_triggered'],
llmUsed: false,
signals: {
procurementSignalsFound: [],
publicBuyerSignalsFound: [],
productSignalsFound: ['Parkscheinautomaten'],
negativesFound: ['Branchenanalyse', 'Experten', 'Marktwachstum']
}
},
isRead: true,
createdAt: new Date(Date.now() - 12 * 60 * 60 * 1000)
},
{
id: 'hit-hec-005',
tenantId: 'hectronic-demo',
topicId: 'hec_ev_charging_v1',
sourceType: 'rss',
sourceRef: 'rss-item-005',
originalUrl: 'https://ausschreibungen.at/ladeinfrastruktur-wien',
canonicalUrl: 'https://ausschreibungen.at/ladeinfrastruktur-wien',
title: 'Stadt Wien: Rahmenvertrag Ladeinfrastruktur öffentlicher Raum',
snippet: 'Vergabeverfahren für Rahmenvertrag über Lieferung und Betrieb von Ladeinfrastruktur im öffentlichen Raum. Details hinter Login.',
foundAt: new Date(Date.now() - 18 * 60 * 60 * 1000),
language: 'de',
countryGuess: 'Austria',
buyerGuess: 'Stadt Wien',
deadlineGuess: undefined,
importanceScore: 55,
importanceLabel: 'WICHTIG',
decisionLabel: 'needs_review',
decisionConfidence: 0.62,
decisionTrace: {
rulesTriggered: ['procurement_signal_match', 'public_buyer_match'],
llmUsed: true,
llmConfidence: 0.62,
signals: {
procurementSignalsFound: ['Vergabeverfahren', 'Rahmenvertrag'],
publicBuyerSignalsFound: ['Stadt Wien', 'öffentlicher Raum'],
productSignalsFound: ['Ladeinfrastruktur'],
negativesFound: ['Details hinter Login']
}
},
isRead: false,
createdAt: new Date(Date.now() - 18 * 60 * 60 * 1000)
},
{
id: 'hit-hec-006',
tenantId: 'hectronic-demo',
topicId: 'hec_fuel_v1',
sourceType: 'email',
sourceRef: 'ga-msg-006',
originalUrl: 'https://vergabe.bund.de/tankautomat-bundeswehr',
canonicalUrl: 'https://vergabe.bund.de/tankautomat-bw',
title: 'Bundesamt für Ausrüstung: Tankautomaten für Liegenschaften',
snippet: 'Beschaffung von unbemannten Tankautomaten für Flottenbetankung an 12 Bundeswehr-Standorten. EU-weite Ausschreibung.',
foundAt: new Date(Date.now() - 24 * 60 * 60 * 1000),
language: 'de',
countryGuess: 'Germany',
buyerGuess: 'Bundesamt für Ausrüstung',
deadlineGuess: '2026-05-15',
importanceScore: 72,
importanceLabel: 'DRINGEND',
decisionLabel: 'relevant',
decisionConfidence: 0.86,
decisionTrace: {
rulesTriggered: ['procurement_signal_match', 'public_buyer_match', 'product_match'],
llmUsed: false,
signals: {
procurementSignalsFound: ['Beschaffung', 'EU-weite Ausschreibung'],
publicBuyerSignalsFound: ['Bundesamt'],
productSignalsFound: ['Tankautomaten', 'Flottenbetankung', 'unbemannt'],
negativesFound: []
}
},
isRead: true,
createdAt: new Date(Date.now() - 24 * 60 * 60 * 1000)
}
]
// ============================================
// DEFAULT SETTINGS
// ============================================
export const defaultB2BSettings: B2BSettings = {
migrationCompleted: false,
wizardCompleted: false,
selectedRegions: ['EUROPE'],
selectedLanguages: ['de', 'en'],
selectedPackages: ['PARKING', 'EV_CHARGING'],
noiseMode: 'STRICT',
notificationCadence: 'DAILY',
notificationTime: '08:00',
maxDigestItems: 10
}
export const defaultTenant: B2BTenant = {
id: 'demo-tenant',
name: 'Demo Account',
companyName: 'Meine Firma GmbH',
industry: 'Payment & Infrastructure',
plan: 'trial',
inboundEmailDomain: 'alerts.breakpilot.de',
createdAt: new Date(),
settings: defaultB2BSettings
}

View File

@@ -0,0 +1,230 @@
// ============================================
// TYPES - B2B Alerts Domain Types
// ============================================
export type ImportanceLabel = 'KRITISCH' | 'DRINGEND' | 'WICHTIG' | 'PRUEFEN' | 'INFO'
export type DecisionLabel = 'relevant' | 'irrelevant' | 'needs_review'
export type SourceType = 'email' | 'rss'
export type NoiseMode = 'STRICT' | 'BALANCED' | 'BROAD'
export type Package = 'PARKING' | 'EV_CHARGING' | 'FUEL' | 'TANK_MONITORING'
export interface AlertSource {
id: string
tenantId: string
type: SourceType
inboundAddress?: string
rssUrl?: string
label: string
active: boolean
createdAt: Date
}
export interface B2BHit {
id: string
tenantId: string
topicId: string
sourceType: SourceType
sourceRef: string
originalUrl: string
canonicalUrl: string
title: string
snippet: string
fullText?: string
foundAt: Date
language?: string
countryGuess?: string
buyerGuess?: string
deadlineGuess?: string
importanceScore: number
importanceLabel: ImportanceLabel
decisionLabel: DecisionLabel
decisionConfidence: number
decisionTrace?: DecisionTrace
isRead: boolean
userFeedback?: 'relevant' | 'irrelevant'
createdAt: Date
}
export interface DecisionTrace {
rulesTriggered: string[]
llmUsed: boolean
llmConfidence?: number
signals: {
procurementSignalsFound: string[]
publicBuyerSignalsFound: string[]
productSignalsFound: string[]
negativesFound: string[]
}
}
export interface B2BTopic {
id: string
tenantId: string
name: string
package: Package
intent: TopicIntent
filters: TopicFilters
decisionPolicy: DecisionPolicy
importanceModelId: string
status: 'active' | 'paused'
createdAt: Date
}
export interface TopicIntent {
goalStatement: string
mustHaveAnyN: number
mustHave: { type: 'keyword'; value: string }[]
publicBuyerSignalsAny: string[]
procurementSignalsAny: string[]
}
export interface TopicFilters {
hardExcludesAny: string[]
softExcludesAny: string[]
}
export interface DecisionPolicy {
rulesFirst: boolean
llmMode: 'ALWAYS' | 'GRAYZONE_ONLY' | 'NEVER'
autoRelevantMinConf: number
needsReviewRange: [number, number]
autoIrrelevantMaxConf: number
}
export interface ImportanceModel {
modelId: string
scoreRange: [number, number]
weights: {
deadlineProximity: number
procurementIntentStrength: number
publicBuyerStrength: number
productMatchStrength: number
sourceTrust: number
novelty: number
}
thresholds: {
infoMax: number
reviewMin: number
importantMin: number
urgentMin: number
criticalMin: number
}
topNCaps: {
dailyDigestMax: number
weeklyDigestMax: number
perTopicDailyMax: number
}
}
export interface B2BTemplate {
templateId: string
templateName: string
templateDescription: string
targetRoles: string[]
industry?: string
companyExample?: string
topics: Omit<B2BTopic, 'id' | 'tenantId' | 'createdAt'>[]
importanceModel: ImportanceModel
guidedConfig: GuidedConfig
notificationPreset: NotificationPreset
}
export interface GuidedConfig {
regionSelector: {
type: 'multi_select'
default: string[]
options: string[]
}
languageSelector: {
type: 'multi_select'
default: string[]
options: string[]
}
packageSelector: {
type: 'multi_select'
default: Package[]
options: Package[]
}
noiseMode: {
type: 'single_select'
default: NoiseMode
options: NoiseMode[]
}
}
export interface NotificationPreset {
presetId: string
channels: string[]
cadence: 'REALTIME' | 'HOURLY' | 'DAILY' | 'WEEKLY'
timeLocal: string
includeStatuses: DecisionLabel[]
maxItems: number
}
export interface B2BTenant {
id: string
name: string
companyName: string
industry: string
plan: 'trial' | 'starter' | 'professional' | 'enterprise'
inboundEmailDomain: string
createdAt: Date
settings: B2BSettings
}
export interface B2BSettings {
migrationCompleted: boolean
wizardCompleted: boolean
selectedTemplateId?: string
selectedRegions: string[]
selectedLanguages: string[]
selectedPackages: Package[]
noiseMode: NoiseMode
notificationCadence: 'REALTIME' | 'HOURLY' | 'DAILY' | 'WEEKLY'
notificationTime: string
maxDigestItems: number
}
export interface AlertsB2BContextType {
// Tenant
tenant: B2BTenant
updateTenant: (updates: Partial<B2BTenant>) => void
// Settings
settings: B2BSettings
updateSettings: (updates: Partial<B2BSettings>) => void
// Templates
availableTemplates: B2BTemplate[]
selectedTemplate: B2BTemplate | null
selectTemplate: (templateId: string) => void
// Sources
sources: AlertSource[]
addSource: (source: Omit<AlertSource, 'id' | 'createdAt'>) => void
removeSource: (id: string) => void
generateInboundEmail: () => string
// Topics
topics: B2BTopic[]
addTopic: (topic: Omit<B2BTopic, 'id' | 'createdAt'>) => void
updateTopic: (id: string, updates: Partial<B2BTopic>) => void
removeTopic: (id: string) => void
// Hits
hits: B2BHit[]
unreadCount: number
relevantCount: number
needsReviewCount: number
markAsRead: (id: string) => void
markAllAsRead: () => void
submitFeedback: (id: string, feedback: 'relevant' | 'irrelevant') => void
processEmailContent: (emailContent: string, emailSubject?: string) => B2BHit
// Digest
getDigest: (maxItems?: number) => B2BHit[]
// Loading/Error
isLoading: boolean
error: string | null
}