[32788f42] feat: implement database persistence, modernized UI with Tailwind, and Calendly-integrated QR card generator for Fotograf.de scraper

This commit is contained in:
2026-03-21 09:04:03 +00:00
parent 22fe4dbd9f
commit c02facdf5d
6975 changed files with 1835694 additions and 179 deletions

View File

@@ -10,25 +10,15 @@ from zoneinfo import ZoneInfo
from threading import Thread, Lock
import uvicorn
import logging
import msal
from fastapi import FastAPI, Request, Response, BackgroundTasks
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from sqlalchemy.orm import sessionmaker
# --- Setup Logging ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
import msal
# Import centralized matching service
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
try:
from company_explorer_connector import match_company
except ImportError:
logging.warning("Could not import company_explorer_connector. Matching logic might be disabled.")
def match_company(name, **kwargs): return {"match_found": False}
# --- Database Setup (SQLite) ---
from .models import ProposalJob, ProposedSlot, init_db
class TestLeadPayload(BaseModel):
company_name: str
@@ -50,6 +40,8 @@ TZ_BERLIN = ZoneInfo("Europe/Berlin")
DB_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "trading_twins.db")
if not os.path.exists(os.path.dirname(DB_FILE_PATH)):
os.makedirs(os.path.dirname(DB_FILE_PATH))
# Initialize SQLAlchemy db properly and get SessionLocal
SessionLocal = init_db(f"sqlite:///{DB_FILE_PATH}")
# --- Config ---
@@ -220,50 +212,101 @@ app = FastAPI()
SMARTLEAD_LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "Log")
SMARTLEAD_LOG_FILE = os.path.join(SMARTLEAD_LOG_DIR, "smartlead_webhooks.log")
import re
# Ensure log directory exists
os.makedirs(SMARTLEAD_LOG_DIR, exist_ok=True)
async def log_webhook_data(webhook_type: str, request: Request):
"""Helper function to log incoming webhook data."""
def clean_html(raw_html):
"""Simple regex to remove HTML tags for cleaner Teams/Log output."""
if not raw_html: return ""
cleanr = re.compile('<.*?>')
cleantext = re.sub(cleanr, '', raw_html)
return cleantext.strip()
async def log_webhook_data(webhook_type: str, request: Request, background_tasks: BackgroundTasks):
"""Helper function to log incoming webhook data and trigger processing."""
try:
data = await request.json()
timestamp = datetime.now(TZ_BERLIN).isoformat()
# Determine real IP from Nginx proxy headers
forwarded_for = request.headers.get("X-Forwarded-For")
real_ip = request.headers.get("X-Real-IP")
source_ip = forwarded_for.split(",")[0].strip() if forwarded_for else (real_ip or request.client.host)
log_entry = {
"timestamp": timestamp,
"webhook_type": webhook_type,
"source_ip": request.client.host,
"source_ip": source_ip,
"payload": data
}
with open(SMARTLEAD_LOG_FILE, "a", encoding="utf-8") as f:
f.write(json.dumps(log_entry) + "\n")
logging.info(f"Successfully processed and logged '{webhook_type}' webhook.")
return {"status": "success", "message": f"{webhook_type} webhook received"}
logging.info(f"Successfully logged '{webhook_type}' webhook.")
# --- Trigger Trading Twins Processing ---
payload = data.get("payload", {})
lead_id = payload.get("lead_id")
if lead_id:
request_id = f"smartlead_{lead_id}"
lead_email = payload.get("lead_email")
lead_data = payload.get("lead_data", {})
company = lead_data.get("company_name", "Unbekannt")
# Construct name
first = lead_data.get("first_name", "")
last = lead_data.get("last_name", "")
full_name = f"{first} {last}".strip() or payload.get("lead_name", "Interessent")
# Extract last reply as context for Teams
last_reply_html = payload.get("last_reply", {}).get("email_body", "")
clean_reply = clean_html(last_reply_html)
# Truncate context for Teams card stability
teams_context = (clean_reply[:300] + '...') if len(clean_reply) > 300 else clean_reply
# Define a default opener for the email (can be refined later)
email_opener = f"Vielen Dank für Ihre Rückmeldung zu unserer Anfrage bezüglich der Reinigungsoptimierung bei {company}."
logging.info(f"Triggering background process for Smartlead Lead {lead_id} ({company})")
background_tasks.add_task(
process_lead,
request_id=request_id,
company=company,
opener=email_opener,
receiver=lead_email,
name=full_name,
teams_context=teams_context
)
return {"status": "success", "message": f"{webhook_type} webhook received and processing triggered"}
except json.JSONDecodeError:
logging.error("Webhook received with invalid JSON format.")
return Response(content='{"status": "error", "message": "Invalid JSON format"}', status_code=400, media_type="application/json")
except Exception as e:
logging.error(f"Error processing '{webhook_type}' webhook: {e}")
logging.error(f"Error processing '{webhook_type}' webhook: {e}", exc_info=True)
return Response(content='{"status": "error", "message": "Internal server error"}', status_code=500, media_type="application/json")
@app.post("/webhook/hot-lead", status_code=202)
async def webhook_hot_lead(request: Request):
async def webhook_hot_lead(request: Request, background_tasks: BackgroundTasks):
"""Webhook endpoint for 'hot leads' from Smartlead."""
return await log_webhook_data("hot-lead", request)
return await log_webhook_data("hot-lead", request, background_tasks)
@app.post("/webhook/follow-up-lead", status_code=202)
async def webhook_follow_up_lead(request: Request):
async def webhook_follow_up_lead(request: Request, background_tasks: BackgroundTasks):
"""Webhook endpoint for 'follow-up leads' from Smartlead."""
return await log_webhook_data("follow-up-lead", request)
return await log_webhook_data("follow-up-lead", request, background_tasks)
# --- END Webhook Endpoints ---
@app.get("/test_lead", status_code=202)
def trigger_test_lead(background_tasks: BackgroundTasks):
req_id = f"test_{int(time.time())}"
background_tasks.add_task(process_lead, req_id, "Testfirma GmbH", "Wir haben Ihre Anfrage erhalten.", TEST_RECEIVER_EMAIL, "Max Mustermann")
background_tasks.add_task(process_lead, req_id, "Testfirma GmbH", "Wir haben Ihre Anfrage erhalten.", TEST_RECEIVER_EMAIL, "Max Mustermann", teams_context="Test-Context von manuellem Trigger")
return {"status": "Test lead triggered", "id": req_id}
@app.post("/test_specific_lead", status_code=202)
@@ -277,7 +320,8 @@ def trigger_specific_test_lead(payload: TestLeadPayload, background_tasks: Backg
company=payload.company_name,
opener=payload.opener,
receiver=TEST_RECEIVER_EMAIL, # <--- FORCED TEST EMAIL
name=payload.contact_name
name=payload.contact_name,
teams_context="Manueller Trigger mit spezifischen Daten"
)
return {"status": "Specific test lead triggered", "id": req_id}
@@ -450,7 +494,7 @@ def send_email(subject, body, to_email):
logging.error(f"Error sending mail: {response.text}")
def process_lead(request_id, company, opener, receiver, name):
def process_lead(request_id, company, opener, receiver, name, teams_context=None):
logging.info(f"--- Starting process_lead for request_id: {request_id} ---")
db = SessionLocal()
try:
@@ -475,9 +519,16 @@ def process_lead(request_id, company, opener, receiver, name):
db.commit()
send_time = datetime.now(TZ_BERLIN) + timedelta(minutes=DEFAULT_WAIT_MINUTES)
logging.info(f"Sending Teams approval card for job {request_id}.")
from .teams_notification import send_approval_card
send_approval_card(job_uuid=request_id, customer_name=company, time_string=send_time.strftime("%H:%M"), webhook_url=TEAMS_WEBHOOK_URL, api_base_url=FEEDBACK_SERVER_BASE_URL)
logging.info(f"Teams approval card for job {request_id} is currently DISABLED.")
# from .teams_notification import send_approval_card
# send_approval_card(
# job_uuid=request_id,
# customer_name=company,
# time_string=send_time.strftime("%H:%M"),
# webhook_url=TEAMS_WEBHOOK_URL,
# api_base_url=FEEDBACK_SERVER_BASE_URL,
# context=teams_context
# )
logging.info(f"Waiting for response or timeout until {send_time.strftime('%H:%M:%S')} for job {request_id}")
wait_until = datetime.now(TZ_BERLIN) + timedelta(minutes=DEFAULT_WAIT_MINUTES)
@@ -538,3 +589,8 @@ def process_lead(request_id, company, opener, receiver, name):
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8004)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8004)

View File

@@ -3,41 +3,50 @@ import json
import os
from datetime import datetime
import logging
# Default-Webhook (Platzhalter) - sollte in .env stehen
DEFAULT_WEBHOOK_URL = os.getenv("TEAMS_WEBHOOK_URL", "")
def send_approval_card(job_uuid, customer_name, time_string, webhook_url=DEFAULT_WEBHOOK_URL, api_base_url="http://localhost:8004"):
def send_approval_card(job_uuid, customer_name, time_string, webhook_url=DEFAULT_WEBHOOK_URL, api_base_url="http://localhost:8004", context=None):
"""
Sendet eine Adaptive Card an Teams mit Approve/Deny Buttons.
"""
# Die URL unserer API (muss von außen erreichbar sein, z.B. via ngrok oder Server-IP)
body_elements = [
{
"type": "TextBlock",
"text": f"🤖 Automatisierte E-Mail an {customer_name} (via Trading Twins) wird um {time_string} Uhr ausgesendet.",
"weight": "Bolder",
"size": "Medium"
}
]
if context:
body_elements.append({
"type": "FactSet",
"facts": [
{"title": "Kontext/Reply:", "value": context}
]
})
body_elements.append({
"type": "TextBlock",
"text": f"Wenn Du bis {time_string} Uhr NICHT reagierst, wird die generierte E-Mail automatisch ausgesendet.",
"isSubtle": True,
"wrap": True
})
card_payload = {
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"contentUrl": None,
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"text": f"🤖 Automatisierte E-Mail an {customer_name} (via Trading Twins) wird um {time_string} Uhr ausgesendet.",
"weight": "Bolder",
"size": "Medium"
},
{
"type": "TextBlock",
"text": f"Wenn Du bis {time_string} Uhr NICHT reagierst, wird die generierte E-Mail automatisch ausgesendet.",
"isSubtle": True,
"wrap": True
}
],
"body": body_elements,
"actions": [
{
"type": "Action.OpenUrl",