[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:
@@ -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()
|
||||
Reference in New Issue
Block a user