[32788f42] feat: implement database persistence, modernized UI with Tailwind, and Calendly-integrated QR card generator for Fotograf.de scraper
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user