[30388f42] Infrastructure Hardening: Repaired CE/Connector DB schema, fixed frontend styling build, implemented robust echo shield in worker v2.1.1, and integrated Lead Engine into gateway.

This commit is contained in:
2026-03-07 14:08:42 +00:00
parent 35c30bc39a
commit d1b77fd2f6
415 changed files with 24100 additions and 13301 deletions

View File

@@ -1,11 +1,22 @@
from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, ForeignKey, Float, Boolean, JSON
from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, ForeignKey, Float, Boolean, JSON, event
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
from datetime import datetime
from .config import settings
# Setup
engine = create_engine(settings.DATABASE_URL, connect_args={"check_same_thread": False})
engine = create_engine(
settings.DATABASE_URL,
connect_args={"check_same_thread": False, "timeout": 30}
)
# Disable mmap to avoid Docker volume issues on Synology
@event.listens_for(engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA mmap_size=0")
cursor.close()
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
@@ -34,6 +45,8 @@ class Company(Base):
industry_ai = Column(String, nullable=True) # The AI suggested industry
# Location (Golden Record)
street = Column(String, nullable=True) # NEW: Street + Number
zip_code = Column(String, nullable=True) # NEW: Postal Code
city = Column(String, nullable=True)
country = Column(String, default="DE")
@@ -68,6 +81,11 @@ class Company(Base):
metric_source_url = Column(Text, nullable=True) # URL where the proof was found
metric_confidence = Column(Float, nullable=True) # 0.0 - 1.0
metric_confidence_reason = Column(Text, nullable=True) # Why is it high/low?
# NEW: AI-generated Marketing Openers
ai_opener = Column(Text, nullable=True)
ai_opener_secondary = Column(Text, nullable=True)
research_dossier = Column(Text, nullable=True)
# Relationships
signals = relationship("Signal", back_populates="company", cascade="all, delete-orphan")
@@ -100,6 +118,9 @@ class Contact(Base):
role = Column(String) # Operativer Entscheider, etc.
status = Column(String, default="") # Marketing Status
# New field for unsubscribe functionality
unsubscribe_token = Column(String, unique=True, index=True, nullable=True)
is_primary = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
@@ -130,6 +151,7 @@ class Industry(Base):
notes = Column(Text, nullable=True)
priority = Column(String, nullable=True) # Replaces old status concept ("Freigegeben")
ops_focus_secondary = Column(Boolean, default=False)
strategy_briefing = Column(Text, nullable=True) # NEW: Strategic context (Miller Heiman)
# NEW SCHEMA FIELDS (from MIGRATION_PLAN)
metric_type = Column(String, nullable=True) # Unit_Count, Area_in, Area_out
@@ -150,17 +172,63 @@ class Industry(Base):
created_at = Column(DateTime, default=datetime.utcnow)
class JobRoleMapping(Base):
class JobRolePattern(Base):
"""
Maps job title patterns (regex or simple string) to Roles.
Maps job title patterns (regex or exact string) to internal Roles.
"""
__tablename__ = "job_role_mappings"
__tablename__ = "job_role_patterns"
id = Column(Integer, primary_key=True, index=True)
pattern = Column(String, unique=True) # e.g. "%CTO%" or "Technischer Leiter"
role = Column(String) # The target Role
pattern_type = Column(String, default="exact", index=True) # 'exact' or 'regex'
pattern_value = Column(String, unique=True) # e.g. "Technischer Leiter" or "(?i)leiter.*technik"
role = Column(String, index=True) # The target Role, maps to Persona.name
priority = Column(Integer, default=100) # Lower number means higher priority
is_active = Column(Boolean, default=True)
created_by = Column(String, default="system") # 'system', 'user', 'llm'
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class RawJobTitle(Base):
"""
Stores raw unique job titles imported from CRM to assist in pattern mining.
Tracks frequency to prioritize high-impact patterns.
"""
__tablename__ = "raw_job_titles"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, unique=True, index=True) # The raw string, e.g. "Senior Sales Mgr."
count = Column(Integer, default=1) # How often this title appears in the CRM
source = Column(String, default="import")
# Status Flags
is_mapped = Column(Boolean, default=False) # True if a pattern currently covers this title
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class Persona(Base):
"""
Represents a generalized persona/role (e.g. 'Geschäftsführer', 'IT-Leiter')
independent of the specific job title pattern.
Stores the strategic messaging components.
"""
__tablename__ = "personas"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, index=True) # Matches the 'role' string in JobRolePattern
description = Column(Text, nullable=True) # NEW: Role description / how they think
pains = Column(Text, nullable=True) # JSON list or multiline string
gains = Column(Text, nullable=True) # JSON list or multiline string
convincing_arguments = Column(Text, nullable=True) # NEW: What convinces them
typical_positions = Column(Text, nullable=True) # NEW: Typical titles
kpis = Column(Text, nullable=True) # NEW: Relevant KPIs
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class Signal(Base):
@@ -254,8 +322,8 @@ class ReportedMistake(Base):
class MarketingMatrix(Base):
"""
Stores the static marketing texts for Industry x Role combinations.
Source: Notion (synced).
Stores the static marketing texts for Industry x Persona combinations.
Source: Generated via AI.
"""
__tablename__ = "marketing_matrix"
@@ -263,7 +331,8 @@ class MarketingMatrix(Base):
# The combination keys
industry_id = Column(Integer, ForeignKey("industries.id"), nullable=False)
role_id = Column(Integer, ForeignKey("job_role_mappings.id"), nullable=False)
persona_id = Column(Integer, ForeignKey("personas.id"), nullable=False)
campaign_tag = Column(String, default="standard", index=True) # NEW: Allows multiple variants (e.g. "standard", "messe_2026", "warmup")
# The Content
subject = Column(Text, nullable=True)
@@ -273,7 +342,7 @@ class MarketingMatrix(Base):
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
industry = relationship("Industry")
role = relationship("JobRoleMapping")
persona = relationship("Persona")
# ==============================================================================
@@ -329,4 +398,4 @@ def get_db():
try:
yield db
finally:
db.close()
db.close()