diff --git a/.dev_session/SESSION_INFO b/.dev_session/SESSION_INFO index bbb05359..c9fe5c05 100644 --- a/.dev_session/SESSION_INFO +++ b/.dev_session/SESSION_INFO @@ -1 +1 @@ -{"task_id": "2ff88f42-8544-8000-8314-c9013414d1d0", "token": "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", "session_start_time": "2026-02-19T08:32:53.260193"} \ No newline at end of file +{"task_id": "30388f42-8544-8088-bc48-e59e9b973e91", "token": "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", "readme_path": null, "session_start_time": "2026-03-07T14:07:47.125445"} \ No newline at end of file diff --git a/Labyrinth.py b/ARCHIVE_legacy_scripts/Labyrinth.py similarity index 100% rename from Labyrinth.py rename to ARCHIVE_legacy_scripts/Labyrinth.py diff --git a/_legacy_gsheets_system/brancheneinstufung2.py b/ARCHIVE_legacy_scripts/_legacy_gsheets_system/brancheneinstufung2.py similarity index 100% rename from _legacy_gsheets_system/brancheneinstufung2.py rename to ARCHIVE_legacy_scripts/_legacy_gsheets_system/brancheneinstufung2.py diff --git a/_legacy_gsheets_system/build_knowledge_base.py b/ARCHIVE_legacy_scripts/_legacy_gsheets_system/build_knowledge_base.py similarity index 100% rename from _legacy_gsheets_system/build_knowledge_base.py rename to ARCHIVE_legacy_scripts/_legacy_gsheets_system/build_knowledge_base.py diff --git a/_legacy_gsheets_system/company_deduplicator.py b/ARCHIVE_legacy_scripts/_legacy_gsheets_system/company_deduplicator.py similarity index 100% rename from _legacy_gsheets_system/company_deduplicator.py rename to ARCHIVE_legacy_scripts/_legacy_gsheets_system/company_deduplicator.py diff --git a/_legacy_gsheets_system/config.py b/ARCHIVE_legacy_scripts/_legacy_gsheets_system/config.py similarity index 100% rename from _legacy_gsheets_system/config.py rename to ARCHIVE_legacy_scripts/_legacy_gsheets_system/config.py diff --git a/_legacy_gsheets_system/contact_grouping.py b/ARCHIVE_legacy_scripts/_legacy_gsheets_system/contact_grouping.py similarity index 100% rename from _legacy_gsheets_system/contact_grouping.py rename to ARCHIVE_legacy_scripts/_legacy_gsheets_system/contact_grouping.py diff --git a/_legacy_gsheets_system/data_processor.py b/ARCHIVE_legacy_scripts/_legacy_gsheets_system/data_processor.py similarity index 100% rename from _legacy_gsheets_system/data_processor.py rename to ARCHIVE_legacy_scripts/_legacy_gsheets_system/data_processor.py diff --git a/_legacy_gsheets_system/expand_knowledge_base.py b/ARCHIVE_legacy_scripts/_legacy_gsheets_system/expand_knowledge_base.py similarity index 100% rename from _legacy_gsheets_system/expand_knowledge_base.py rename to ARCHIVE_legacy_scripts/_legacy_gsheets_system/expand_knowledge_base.py diff --git a/_legacy_gsheets_system/extract_insights.py b/ARCHIVE_legacy_scripts/_legacy_gsheets_system/extract_insights.py similarity index 100% rename from _legacy_gsheets_system/extract_insights.py rename to ARCHIVE_legacy_scripts/_legacy_gsheets_system/extract_insights.py diff --git a/_legacy_gsheets_system/generate_knowledge_base.py b/ARCHIVE_legacy_scripts/_legacy_gsheets_system/generate_knowledge_base.py similarity index 100% rename from _legacy_gsheets_system/generate_knowledge_base.py rename to ARCHIVE_legacy_scripts/_legacy_gsheets_system/generate_knowledge_base.py diff --git a/_legacy_gsheets_system/generate_marketing_text.py b/ARCHIVE_legacy_scripts/_legacy_gsheets_system/generate_marketing_text.py similarity index 100% rename from _legacy_gsheets_system/generate_marketing_text.py rename to ARCHIVE_legacy_scripts/_legacy_gsheets_system/generate_marketing_text.py diff --git a/_legacy_gsheets_system/google_sheet_handler.py b/ARCHIVE_legacy_scripts/_legacy_gsheets_system/google_sheet_handler.py similarity index 100% rename from _legacy_gsheets_system/google_sheet_handler.py rename to ARCHIVE_legacy_scripts/_legacy_gsheets_system/google_sheet_handler.py diff --git a/_legacy_gsheets_system/helpers.py b/ARCHIVE_legacy_scripts/_legacy_gsheets_system/helpers.py similarity index 100% rename from _legacy_gsheets_system/helpers.py rename to ARCHIVE_legacy_scripts/_legacy_gsheets_system/helpers.py diff --git a/_legacy_gsheets_system/knowledge_base_builder.py b/ARCHIVE_legacy_scripts/_legacy_gsheets_system/knowledge_base_builder.py similarity index 100% rename from _legacy_gsheets_system/knowledge_base_builder.py rename to ARCHIVE_legacy_scripts/_legacy_gsheets_system/knowledge_base_builder.py diff --git a/_legacy_gsheets_system/sync_manager.py b/ARCHIVE_legacy_scripts/_legacy_gsheets_system/sync_manager.py similarity index 100% rename from _legacy_gsheets_system/sync_manager.py rename to ARCHIVE_legacy_scripts/_legacy_gsheets_system/sync_manager.py diff --git a/_legacy_gsheets_system/wikipedia_scraper.py b/ARCHIVE_legacy_scripts/_legacy_gsheets_system/wikipedia_scraper.py similarity index 100% rename from _legacy_gsheets_system/wikipedia_scraper.py rename to ARCHIVE_legacy_scripts/_legacy_gsheets_system/wikipedia_scraper.py diff --git a/brancheneinstufung - Kopie.py b/ARCHIVE_legacy_scripts/brancheneinstufung - Kopie.py similarity index 100% rename from brancheneinstufung - Kopie.py rename to ARCHIVE_legacy_scripts/brancheneinstufung - Kopie.py diff --git a/cat_log.py b/ARCHIVE_legacy_scripts/cat_log.py similarity index 100% rename from cat_log.py rename to ARCHIVE_legacy_scripts/cat_log.py diff --git a/ARCHIVE_legacy_scripts/check_benni.py b/ARCHIVE_legacy_scripts/check_benni.py new file mode 100644 index 00000000..7f24607d --- /dev/null +++ b/ARCHIVE_legacy_scripts/check_benni.py @@ -0,0 +1,40 @@ +import sqlite3 +import os +import json + +DB_PATH = "companies_v3_fixed_2.db" + +def check_company_33(): + if not os.path.exists(DB_PATH): + print(f"❌ Database not found at {DB_PATH}") + return + + try: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + print(f"🔍 Checking Company ID 33 (Bennis Playland)...") + # Check standard fields + cursor.execute("SELECT id, name, city, street, zip_code FROM companies WHERE id = 33") + row = cursor.fetchone() + if row: + print(f" Standard: City='{row[2]}', Street='{row[3]}', Zip='{row[4]}'") + else: + print(" ❌ Company 33 not found in DB.") + + # Check Enrichment + cursor.execute("SELECT content FROM enrichment_data WHERE company_id = 33 AND source_type = 'website_scrape'") + enrich_row = cursor.fetchone() + if enrich_row: + data = json.loads(enrich_row[0]) + imp = data.get("impressum") + print(f" Impressum Data: {json.dumps(imp, indent=2) if imp else 'None'}") + else: + print(" ❌ No website_scrape found for Company 33.") + + conn.close() + except Exception as e: + print(f"❌ Error: {e}") + +if __name__ == "__main__": + check_company_33() diff --git a/check_db.py b/ARCHIVE_legacy_scripts/check_db.py similarity index 100% rename from check_db.py rename to ARCHIVE_legacy_scripts/check_db.py diff --git a/check_db_content.py b/ARCHIVE_legacy_scripts/check_db_content.py similarity index 100% rename from check_db_content.py rename to ARCHIVE_legacy_scripts/check_db_content.py diff --git a/ARCHIVE_legacy_scripts/check_erding_openers.py b/ARCHIVE_legacy_scripts/check_erding_openers.py new file mode 100644 index 00000000..9e531aca --- /dev/null +++ b/ARCHIVE_legacy_scripts/check_erding_openers.py @@ -0,0 +1,16 @@ +import sqlite3 + +DB_PATH = "/app/companies_v3_fixed_2.db" +conn = sqlite3.connect(DB_PATH) +cursor = conn.cursor() + +cursor.execute("SELECT name, ai_opener, ai_opener_secondary, industry_ai FROM companies WHERE name LIKE '%Erding%'") +row = cursor.fetchone() +if row: + print(f"Company: {row[0]}") + print(f"Industry: {row[3]}") + print(f"Opener Primary: {row[1]}") + print(f"Opener Secondary: {row[2]}") +else: + print("Company not found.") +conn.close() diff --git a/ARCHIVE_legacy_scripts/check_klinikum_erding.py b/ARCHIVE_legacy_scripts/check_klinikum_erding.py new file mode 100644 index 00000000..4d37cffe --- /dev/null +++ b/ARCHIVE_legacy_scripts/check_klinikum_erding.py @@ -0,0 +1,16 @@ +import sqlite3 + +DB_PATH = "/app/companies_v3_fixed_2.db" +conn = sqlite3.connect(DB_PATH) +cursor = conn.cursor() + +cursor.execute("SELECT name, ai_opener, ai_opener_secondary, industry_ai FROM companies WHERE name LIKE '%Klinikum Landkreis Erding%'") +row = cursor.fetchone() +if row: + print(f"Company: {row[0]}") + print(f"Industry: {row[3]}") + print(f"Opener Primary: {row[1]}") + print(f"Opener Secondary: {row[2]}") +else: + print("Company not found.") +conn.close() diff --git a/ARCHIVE_legacy_scripts/check_mappings.py b/ARCHIVE_legacy_scripts/check_mappings.py new file mode 100644 index 00000000..b1dfedd9 --- /dev/null +++ b/ARCHIVE_legacy_scripts/check_mappings.py @@ -0,0 +1,14 @@ +import sqlite3 + +def check_mappings(): + conn = sqlite3.connect('/app/companies_v3_fixed_2.db') + cursor = conn.cursor() + cursor.execute("SELECT * FROM job_role_mappings") + rows = cursor.fetchall() + print("--- Job Role Mappings ---") + for row in rows: + print(row) + conn.close() + +if __name__ == "__main__": + check_mappings() diff --git a/ARCHIVE_legacy_scripts/check_matrix.py b/ARCHIVE_legacy_scripts/check_matrix.py new file mode 100644 index 00000000..bc8f397b --- /dev/null +++ b/ARCHIVE_legacy_scripts/check_matrix.py @@ -0,0 +1,25 @@ +import os +import sys + +# Add the company-explorer directory to the Python path +sys.path.append(os.path.abspath(os.path.join(os.getcwd(), 'company-explorer'))) + +from backend.database import SessionLocal, MarketingMatrix, Industry, Persona +import json + +db = SessionLocal() +try: + count = db.query(MarketingMatrix).count() + print(f"MarketingMatrix count: {count}") + + if count > 0: + first = db.query(MarketingMatrix).first() + print(f"First entry: ID={first.id}, Industry={first.industry_id}, Persona={first.persona_id}") + else: + print("MarketingMatrix is empty.") + # Check if we have industries and personas + ind_count = db.query(Industry).count() + pers_count = db.query(Persona).count() + print(f"Industries: {ind_count}, Personas: {pers_count}") +finally: + db.close() \ No newline at end of file diff --git a/ARCHIVE_legacy_scripts/check_matrix_indoor.py b/ARCHIVE_legacy_scripts/check_matrix_indoor.py new file mode 100644 index 00000000..b8e1f3e4 --- /dev/null +++ b/ARCHIVE_legacy_scripts/check_matrix_indoor.py @@ -0,0 +1,23 @@ +import sqlite3 + +DB_PATH = "/app/companies_v3_fixed_2.db" +conn = sqlite3.connect(DB_PATH) +cursor = conn.cursor() + +query = """ +SELECT i.name, p.name, m.subject, m.intro, m.social_proof +FROM marketing_matrix m +JOIN industries i ON m.industry_id = i.id +JOIN personas p ON m.persona_id = p.id +WHERE i.name = 'Leisure - Indoor Active' +""" + +cursor.execute(query) +rows = cursor.fetchall() +for row in rows: + print(f"Industry: {row[0]} | Persona: {row[1]}") + print(f" Subject: {row[2]}") + print(f" Intro: {row[3]}") + print(f" Social Proof: {row[4]}") + print("-" * 50) +conn.close() diff --git a/ARCHIVE_legacy_scripts/check_matrix_results.py b/ARCHIVE_legacy_scripts/check_matrix_results.py new file mode 100644 index 00000000..c407559e --- /dev/null +++ b/ARCHIVE_legacy_scripts/check_matrix_results.py @@ -0,0 +1,24 @@ +import sqlite3 +import json + +DB_PATH = "/app/companies_v3_fixed_2.db" +conn = sqlite3.connect(DB_PATH) +cursor = conn.cursor() + +query = """ +SELECT i.name, p.name, m.subject, m.intro, m.social_proof +FROM marketing_matrix m +JOIN industries i ON m.industry_id = i.id +JOIN personas p ON m.persona_id = p.id +WHERE i.name = 'Healthcare - Hospital' +""" + +cursor.execute(query) +rows = cursor.fetchall() +for row in rows: + print(f"Industry: {row[0]} | Persona: {row[1]}") + print(f" Subject: {row[2]}") + print(f" Intro: {row[3]}") + print(f" Social Proof: {row[4]}") + print("-" * 50) +conn.close() diff --git a/check_schema.py b/ARCHIVE_legacy_scripts/check_schema.py similarity index 100% rename from check_schema.py rename to ARCHIVE_legacy_scripts/check_schema.py diff --git a/ARCHIVE_legacy_scripts/check_silly_billy.py b/ARCHIVE_legacy_scripts/check_silly_billy.py new file mode 100644 index 00000000..2f1db0dd --- /dev/null +++ b/ARCHIVE_legacy_scripts/check_silly_billy.py @@ -0,0 +1,53 @@ +import sqlite3 +import os + +DB_PATH = "companies_v3_fixed_2.db" + +def check_company(): + if not os.path.exists(DB_PATH): + print(f"❌ Database not found at {DB_PATH}") + return + + try: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + print(f"🔍 Searching for 'Silly Billy' in {DB_PATH}...") + cursor.execute("SELECT id, name, crm_id, ai_opener, ai_opener_secondary, city, crm_vat, status FROM companies WHERE name LIKE '%Silly Billy%'") + rows = cursor.fetchall() + + if not rows: + print("❌ No company found matching 'Silly Billy'") + else: + for row in rows: + company_id = row[0] + print("\n✅ Company Found:") + print(f" ID: {company_id}") + print(f" Name: {row[1]}") + print(f" CRM ID: {row[2]}") + print(f" Status: {row[7]}") + print(f" City: {row[5]}") + print(f" VAT: {row[6]}") + print(f" Opener (Primary): {row[3][:50]}..." if row[3] else " Opener (Primary): None") + + # Check Enrichment Data + print(f"\n 🔍 Checking Enrichment Data for ID {company_id}...") + cursor.execute("SELECT content FROM enrichment_data WHERE company_id = ? AND source_type = 'website_scrape'", (company_id,)) + enrich_row = cursor.fetchone() + if enrich_row: + import json + try: + data = json.loads(enrich_row[0]) + imp = data.get("impressum") + print(f" Impressum Data in Scrape: {json.dumps(imp, indent=2) if imp else 'None'}") + except Exception as e: + print(f" ❌ Error parsing JSON: {e}") + else: + print(" ❌ No website_scrape enrichment data found.") + + conn.close() + except Exception as e: + print(f"❌ Error reading DB: {e}") + +if __name__ == "__main__": + check_company() diff --git a/check_syntax.py b/ARCHIVE_legacy_scripts/check_syntax.py similarity index 100% rename from check_syntax.py rename to ARCHIVE_legacy_scripts/check_syntax.py diff --git a/clean_file.py b/ARCHIVE_legacy_scripts/clean_file.py similarity index 100% rename from clean_file.py rename to ARCHIVE_legacy_scripts/clean_file.py diff --git a/ARCHIVE_legacy_scripts/clear_zombies.py b/ARCHIVE_legacy_scripts/clear_zombies.py new file mode 100644 index 00000000..cb3573ee --- /dev/null +++ b/ARCHIVE_legacy_scripts/clear_zombies.py @@ -0,0 +1,31 @@ +import sqlite3 +from datetime import datetime, timedelta + +DB_PATH = "/app/connector_queue.db" + +def clear_all_zombies(): + print("🧹 Cleaning up Zombie Jobs (PROCESSING for too long)...") + # A job that is PROCESSING for more than 10 minutes is likely dead + threshold = (datetime.utcnow() - timedelta(minutes=10)).strftime('%Y-%m-%d %H:%M:%S') + + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + + # 1. Identify Zombies + cursor.execute("SELECT id, updated_at FROM jobs WHERE status = 'PROCESSING' AND updated_at < ?", (threshold,)) + zombies = cursor.fetchall() + + if not zombies: + print("✅ No zombies found.") + return + + print(f"🕵️ Found {len(zombies)} zombie jobs.") + for zid, updated in zombies: + print(f" - Zombie ID {zid} (Last active: {updated})") + + # 2. Kill them + cursor.execute("UPDATE jobs SET status = 'FAILED', error_msg = 'Zombie cleared: Process timed out' WHERE status = 'PROCESSING' AND updated_at < ?", (threshold,)) + print(f"✅ Successfully cleared {cursor.rowcount} zombie(s).") + +if __name__ == "__main__": + clear_all_zombies() diff --git a/create_weights.py b/ARCHIVE_legacy_scripts/create_weights.py similarity index 100% rename from create_weights.py rename to ARCHIVE_legacy_scripts/create_weights.py diff --git a/dealfront_enrichment.py b/ARCHIVE_legacy_scripts/dealfront_enrichment.py similarity index 100% rename from dealfront_enrichment.py rename to ARCHIVE_legacy_scripts/dealfront_enrichment.py diff --git a/ARCHIVE_legacy_scripts/debug_connector_status.py b/ARCHIVE_legacy_scripts/debug_connector_status.py new file mode 100644 index 00000000..472f22b5 --- /dev/null +++ b/ARCHIVE_legacy_scripts/debug_connector_status.py @@ -0,0 +1,49 @@ +import sqlite3 +import json +import os + +DB_PATH = "connector_queue.db" + +def inspect_queue(): + if not os.path.exists(DB_PATH): + print(f"❌ Database not found at {DB_PATH}") + return + + print(f"🔍 Inspecting Queue: {DB_PATH}") + try: + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + # Get stats + cursor.execute("SELECT status, COUNT(*) FROM jobs GROUP BY status") + stats = dict(cursor.fetchall()) + print(f"\n📊 Stats: {stats}") + + # Get recent jobs + print("\n📝 Last 10 Jobs:") + cursor.execute("SELECT id, event_type, status, error_msg, updated_at, payload FROM jobs ORDER BY updated_at DESC LIMIT 10") + rows = cursor.fetchall() + + for row in rows: + payload = json.loads(row['payload']) + # Try to identify entity + entity = "Unknown" + if "PrimaryKey" in payload: entity = f"ID {payload['PrimaryKey']}" + if "ContactId" in payload: entity = f"Contact {payload['ContactId']}" + + print(f" - Job #{row['id']} [{row['status']}] {row['event_type']} ({entity})") + print(f" Updated: {row['updated_at']}") + if row['error_msg']: + print(f" ❌ ERROR: {row['error_msg']}") + + # Print payload details relevant to syncing + if row['status'] == 'COMPLETED': + pass # Maybe less interesting if success, but user says it didn't sync + + conn.close() + except Exception as e: + print(f"❌ Error reading DB: {e}") + +if __name__ == "__main__": + inspect_queue() diff --git a/debug_igepa.py b/ARCHIVE_legacy_scripts/debug_igepa.py similarity index 100% rename from debug_igepa.py rename to ARCHIVE_legacy_scripts/debug_igepa.py diff --git a/debug_igepa_deep.py b/ARCHIVE_legacy_scripts/debug_igepa_deep.py similarity index 100% rename from debug_igepa_deep.py rename to ARCHIVE_legacy_scripts/debug_igepa_deep.py diff --git a/debug_igepa_dump.py b/ARCHIVE_legacy_scripts/debug_igepa_dump.py similarity index 100% rename from debug_igepa_dump.py rename to ARCHIVE_legacy_scripts/debug_igepa_dump.py diff --git a/debug_meeting.py b/ARCHIVE_legacy_scripts/debug_meeting.py similarity index 100% rename from debug_meeting.py rename to ARCHIVE_legacy_scripts/debug_meeting.py diff --git a/ARCHIVE_legacy_scripts/debug_paths.py b/ARCHIVE_legacy_scripts/debug_paths.py new file mode 100644 index 00000000..18607e1d --- /dev/null +++ b/ARCHIVE_legacy_scripts/debug_paths.py @@ -0,0 +1,13 @@ +import os +static_path = "/frontend_static" +print(f"Path {static_path} exists: {os.path.exists(static_path)}") +if os.path.exists(static_path): + for root, dirs, files in os.walk(static_path): + for file in files: + print(os.path.join(root, file)) +else: + print("Listing /app instead:") + for root, dirs, files in os.walk("/app"): + if "node_modules" in root: continue + for file in files: + print(os.path.join(root, file)) diff --git a/debug_screenshot.py b/ARCHIVE_legacy_scripts/debug_screenshot.py similarity index 100% rename from debug_screenshot.py rename to ARCHIVE_legacy_scripts/debug_screenshot.py diff --git a/debug_transcription_raw.py b/ARCHIVE_legacy_scripts/debug_transcription_raw.py similarity index 100% rename from debug_transcription_raw.py rename to ARCHIVE_legacy_scripts/debug_transcription_raw.py diff --git a/ARCHIVE_legacy_scripts/debug_zombie.py b/ARCHIVE_legacy_scripts/debug_zombie.py new file mode 100644 index 00000000..69e2d013 --- /dev/null +++ b/ARCHIVE_legacy_scripts/debug_zombie.py @@ -0,0 +1,16 @@ +import sqlite3 +import os + +DB_PATH = "/app/connector_queue.db" + +if __name__ == "__main__": + print(f"📊 Accessing database at {DB_PATH}") + print("📊 Listing last 20 jobs in database...") + with sqlite3.connect(DB_PATH) as conn: + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute("SELECT id, status, event_type, updated_at FROM jobs ORDER BY id DESC LIMIT 20") + rows = cursor.fetchall() + for r in rows: + print(f" - Job {r['id']}: {r['status']} ({r['event_type']}) - Updated: {r['updated_at']}") + diff --git a/duplicate_checker.py b/ARCHIVE_legacy_scripts/duplicate_checker.py similarity index 100% rename from duplicate_checker.py rename to ARCHIVE_legacy_scripts/duplicate_checker.py diff --git a/ARCHIVE_legacy_scripts/fix_benni_data.py b/ARCHIVE_legacy_scripts/fix_benni_data.py new file mode 100644 index 00000000..a8ebe42f --- /dev/null +++ b/ARCHIVE_legacy_scripts/fix_benni_data.py @@ -0,0 +1,41 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +import json + +# Setup DB +DB_PATH = "sqlite:///companies_v3_fixed_2.db" +engine = create_engine(DB_PATH) +SessionLocal = sessionmaker(bind=engine) +session = SessionLocal() + +from sqlalchemy import Column, Integer, String +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +class Company(Base): + __tablename__ = "companies" + id = Column(Integer, primary_key=True) + street = Column(String) + zip_code = Column(String) + +def fix_benni(): + company_id = 33 + print(f"🔧 Fixing Address for Company ID {company_id}...") + + company = session.query(Company).filter_by(id=company_id).first() + if not company: + print("❌ Company not found.") + return + + # Hardcoded from previous check_benni.py output to be safe/fast + # "street": "Eriagstraße 58", "zip": "85053" + + company.street = "Eriagstraße 58" + company.zip_code = "85053" + + session.commit() + print(f"✅ Database updated: Street='{company.street}', Zip='{company.zip_code}'") + +if __name__ == "__main__": + fix_benni() diff --git a/ARCHIVE_legacy_scripts/fix_industry_units.py b/ARCHIVE_legacy_scripts/fix_industry_units.py new file mode 100644 index 00000000..de080761 --- /dev/null +++ b/ARCHIVE_legacy_scripts/fix_industry_units.py @@ -0,0 +1,70 @@ +import sqlite3 + +DB_PATH = "companies_v3_fixed_2.db" + +UNIT_MAPPING = { + "Logistics - Warehouse": "m²", + "Healthcare - Hospital": "Betten", + "Infrastructure - Transport": "Passagiere", + "Leisure - Indoor Active": "m²", + "Retail - Food": "m²", + "Retail - Shopping Center": "m²", + "Hospitality - Gastronomy": "Sitzplätze", + "Leisure - Outdoor Park": "Besucher", + "Leisure - Wet & Spa": "Besucher", + "Infrastructure - Public": "Kapazität", + "Retail - Non-Food": "m²", + "Hospitality - Hotel": "Zimmer", + "Leisure - Entertainment": "Besucher", + "Healthcare - Care Home": "Plätze", + "Industry - Manufacturing": "Mitarbeiter", + "Energy - Grid & Utilities": "Kunden", + "Leisure - Fitness": "Mitglieder", + "Corporate - Campus": "Mitarbeiter", + "Energy - Solar/Wind": "MWp", + "Tech - Data Center": "Racks", + "Automotive - Dealer": "Fahrzeuge", + "Infrastructure Parking": "Stellplätze", + "Reinigungsdienstleister": "Mitarbeiter", + "Infrastructure - Communities": "Einwohner" +} + +def fix_units(): + print(f"Connecting to {DB_PATH}...") + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + try: + cursor.execute("SELECT id, name, scraper_search_term, metric_type FROM industries") + rows = cursor.fetchall() + + updated_count = 0 + + for row in rows: + ind_id, name, current_term, m_type = row + + new_term = UNIT_MAPPING.get(name) + + # Fallback Logic + if not new_term: + if m_type in ["AREA_IN", "AREA_OUT"]: + new_term = "m²" + else: + new_term = "Anzahl" # Generic fallback + + if current_term != new_term: + print(f"Updating '{name}': '{current_term}' -> '{new_term}'") + cursor.execute("UPDATE industries SET scraper_search_term = ? WHERE id = ?", (new_term, ind_id)) + updated_count += 1 + + conn.commit() + print(f"\n✅ Updated {updated_count} industries with correct units.") + + except Exception as e: + print(f"❌ Error: {e}") + conn.rollback() + finally: + conn.close() + +if __name__ == "__main__": + fix_units() diff --git a/ARCHIVE_legacy_scripts/fix_mappings_v2.py b/ARCHIVE_legacy_scripts/fix_mappings_v2.py new file mode 100644 index 00000000..a85fc466 --- /dev/null +++ b/ARCHIVE_legacy_scripts/fix_mappings_v2.py @@ -0,0 +1,23 @@ +import sqlite3 + +def fix_mappings(): + conn = sqlite3.connect('/app/companies_v3_fixed_2.db') + cursor = conn.cursor() + + # Neue Mappings für Geschäftsleitung und Verallgemeinerung + new_rules = [ + ('%leitung%', 'Wirtschaftlicher Entscheider'), + ('%vorstand%', 'Wirtschaftlicher Entscheider'), + ('%geschäftsleitung%', 'Wirtschaftlicher Entscheider'), + ('%management%', 'Wirtschaftlicher Entscheider') + ] + + for pattern, role in new_rules: + cursor.execute("INSERT OR REPLACE INTO job_role_mappings (pattern, role, created_at) VALUES (?, ?, '2026-02-22T15:30:00')", (pattern, role)) + + conn.commit() + conn.close() + print("Mappings updated for Geschäftsleitung, Vorstand, Management.") + +if __name__ == "__main__": + fix_mappings() diff --git a/ARCHIVE_legacy_scripts/fix_silly_billy_data.py b/ARCHIVE_legacy_scripts/fix_silly_billy_data.py new file mode 100644 index 00000000..5908bc33 --- /dev/null +++ b/ARCHIVE_legacy_scripts/fix_silly_billy_data.py @@ -0,0 +1,90 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +import json +import logging + +# Setup DB +DB_PATH = "sqlite:///companies_v3_fixed_2.db" +engine = create_engine(DB_PATH) +SessionLocal = sessionmaker(bind=engine) +session = SessionLocal() + +# Import Models (Simplified for script) +from sqlalchemy import Column, Integer, String, Text, JSON +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +class Company(Base): + __tablename__ = "companies" + id = Column(Integer, primary_key=True) + name = Column(String) + city = Column(String) + country = Column(String) + crm_vat = Column(String) + street = Column(String) + zip_code = Column(String) + +class EnrichmentData(Base): + __tablename__ = "enrichment_data" + id = Column(Integer, primary_key=True) + company_id = Column(Integer) + source_type = Column(String) + content = Column(JSON) + +def fix_data(): + company_id = 32 + print(f"🔧 Fixing Data for Company ID {company_id}...") + + company = session.query(Company).filter_by(id=company_id).first() + if not company: + print("❌ Company not found.") + return + + enrichment = session.query(EnrichmentData).filter_by( + company_id=company_id, source_type="website_scrape" + ).first() + + if enrichment and enrichment.content: + imp = enrichment.content.get("impressum") + if imp: + print(f"📄 Found Impressum: {imp}") + + changed = False + if imp.get("city"): + company.city = imp.get("city") + changed = True + print(f" -> Set City: {company.city}") + + if imp.get("vat_id"): + company.crm_vat = imp.get("vat_id") + changed = True + print(f" -> Set VAT: {company.crm_vat}") + + if imp.get("country_code"): + company.country = imp.get("country_code") + changed = True + print(f" -> Set Country: {company.country}") + + if imp.get("street"): + company.street = imp.get("street") + changed = True + print(f" -> Set Street: {company.street}") + + if imp.get("zip"): + company.zip_code = imp.get("zip") + changed = True + print(f" -> Set Zip: {company.zip_code}") + + if changed: + session.commit() + print("✅ Database updated.") + else: + print("ℹ️ No changes needed.") + else: + print("⚠️ No impressum data in enrichment.") + else: + print("⚠️ No enrichment data found.") + +if __name__ == "__main__": + fix_data() diff --git a/gtm_architect_orchestrator.py b/ARCHIVE_legacy_scripts/gtm_architect_orchestrator.py similarity index 100% rename from gtm_architect_orchestrator.py rename to ARCHIVE_legacy_scripts/gtm_architect_orchestrator.py diff --git a/gtm_db_manager.py b/ARCHIVE_legacy_scripts/gtm_db_manager.py similarity index 100% rename from gtm_db_manager.py rename to ARCHIVE_legacy_scripts/gtm_db_manager.py diff --git a/ARCHIVE_legacy_scripts/list_all_companies.py b/ARCHIVE_legacy_scripts/list_all_companies.py new file mode 100644 index 00000000..dad1fa55 --- /dev/null +++ b/ARCHIVE_legacy_scripts/list_all_companies.py @@ -0,0 +1,30 @@ +import sqlite3 +import os + +DB_PATH = "companies_v3_fixed_2.db" + +def list_companies(): + if not os.path.exists(DB_PATH): + print(f"❌ Database not found at {DB_PATH}") + return + + try: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + print(f"🔍 Listing companies in {DB_PATH}...") + cursor.execute("SELECT id, name, crm_id, city, crm_vat FROM companies ORDER BY id DESC LIMIT 20") + rows = cursor.fetchall() + + if not rows: + print("❌ No companies found") + else: + for row in rows: + print(f" ID: {row[0]} | Name: {row[1]} | CRM ID: {row[2]} | City: {row[3]} | VAT: {row[4]}") + + conn.close() + except Exception as e: + print(f"❌ Error reading DB: {e}") + +if __name__ == "__main__": + list_companies() diff --git a/ARCHIVE_legacy_scripts/list_industries.py b/ARCHIVE_legacy_scripts/list_industries.py new file mode 100644 index 00000000..bf7c5338 --- /dev/null +++ b/ARCHIVE_legacy_scripts/list_industries.py @@ -0,0 +1,18 @@ + +import sys +import os +sys.path.append(os.path.join(os.path.dirname(__file__), "company-explorer")) +from backend.database import SessionLocal, Industry + +def list_industries(): + db = SessionLocal() + try: + industries = db.query(Industry.name).all() + print("Available Industries:") + for (name,) in industries: + print(f"- {name}") + finally: + db.close() + +if __name__ == "__main__": + list_industries() diff --git a/ARCHIVE_legacy_scripts/list_industries_db.py b/ARCHIVE_legacy_scripts/list_industries_db.py new file mode 100644 index 00000000..bf8102cd --- /dev/null +++ b/ARCHIVE_legacy_scripts/list_industries_db.py @@ -0,0 +1,12 @@ +import sqlite3 + +DB_PATH = "/app/companies_v3_fixed_2.db" +conn = sqlite3.connect(DB_PATH) +cursor = conn.cursor() + +cursor.execute("SELECT name FROM industries") +industries = cursor.fetchall() +print("Available Industries:") +for ind in industries: + print(f"- {ind[0]}") +conn.close() diff --git a/ARCHIVE_legacy_scripts/market_db_manager.py b/ARCHIVE_legacy_scripts/market_db_manager.py new file mode 100644 index 00000000..1055ff0d --- /dev/null +++ b/ARCHIVE_legacy_scripts/market_db_manager.py @@ -0,0 +1,120 @@ +import sqlite3 +import json +import os +import uuid +from datetime import datetime + +DB_PATH = os.environ.get("DB_PATH", "/app/market_intelligence.db") + +def get_db_connection(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn + +def init_db(): + conn = get_db_connection() + # Flexible schema: We store almost everything in a 'data' JSON column + conn.execute(''' + CREATE TABLE IF NOT EXISTS projects ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + data JSON NOT NULL + ) + ''') + conn.commit() + conn.close() + +def save_project(project_data): + """ + Saves a project. If 'id' exists in data, updates it. Otherwise creates new. + """ + conn = get_db_connection() + try: + project_id = project_data.get('id') + + # Extract a name for the list view (e.g. from companyName or referenceUrl) + # We assume the frontend passes a 'name' field, or we derive it. + name = project_data.get('name') or project_data.get('companyName') or "Untitled Project" + + if not project_id: + # Create New + project_id = str(uuid.uuid4()) + project_data['id'] = project_id + + conn.execute( + 'INSERT INTO projects (id, name, data) VALUES (?, ?, ?)', + (project_id, name, json.dumps(project_data)) + ) + else: + # Update Existing + conn.execute( + '''UPDATE projects + SET name = ?, data = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ?''', + (name, json.dumps(project_data), project_id) + ) + + conn.commit() + return {"id": project_id, "status": "saved"} + + except Exception as e: + return {"error": str(e)} + finally: + conn.close() + +def get_all_projects(): + conn = get_db_connection() + projects = conn.execute('SELECT id, name, created_at, updated_at FROM projects ORDER BY updated_at DESC').fetchall() + conn.close() + return [dict(ix) for ix in projects] + +def load_project(project_id): + conn = get_db_connection() + project = conn.execute('SELECT data FROM projects WHERE id = ?', (project_id,)).fetchone() + conn.close() + if project: + return json.loads(project['data']) + return None + +def delete_project(project_id): + conn = get_db_connection() + try: + conn.execute('DELETE FROM projects WHERE id = ?', (project_id,)) + conn.commit() + return {"status": "deleted", "id": project_id} + except Exception as e: + return {"error": str(e)} + finally: + conn.close() + +if __name__ == "__main__": + import sys + # Simple CLI for Node.js bridge + # Usage: python market_db_manager.py [init|list|save|load|delete] [args...] + + mode = sys.argv[1] + + if mode == "init": + init_db() + print(json.dumps({"status": "initialized"})) + + elif mode == "list": + print(json.dumps(get_all_projects())) + + elif mode == "save": + # Data is passed as a JSON string file path to avoid command line length limits + data_file = sys.argv[2] + with open(data_file, 'r') as f: + data = json.load(f) + print(json.dumps(save_project(data))) + + elif mode == "load": + p_id = sys.argv[2] + result = load_project(p_id) + print(json.dumps(result if result else {"error": "Project not found"})) + + elif mode == "delete": + p_id = sys.argv[2] + print(json.dumps(delete_project(p_id))) diff --git a/market_intel_orchestrator.py b/ARCHIVE_legacy_scripts/market_intel_orchestrator.py similarity index 100% rename from market_intel_orchestrator.py rename to ARCHIVE_legacy_scripts/market_intel_orchestrator.py diff --git a/ARCHIVE_legacy_scripts/migrate_opener_native.py b/ARCHIVE_legacy_scripts/migrate_opener_native.py new file mode 100644 index 00000000..1a90e283 --- /dev/null +++ b/ARCHIVE_legacy_scripts/migrate_opener_native.py @@ -0,0 +1,29 @@ +import sqlite3 +import sys + +DB_PATH = "/app/companies_v3_fixed_2.db" + +def migrate(): + try: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + print(f"Checking schema in {DB_PATH}...") + cursor.execute("PRAGMA table_info(companies)") + columns = [row[1] for row in cursor.fetchall()] + + if "ai_opener" in columns: + print("Column 'ai_opener' already exists. Skipping.") + else: + print("Adding column 'ai_opener' to 'companies' table...") + cursor.execute("ALTER TABLE companies ADD COLUMN ai_opener TEXT") + conn.commit() + print("✅ Migration successful.") + + except Exception as e: + print(f"❌ Migration failed: {e}") + finally: + if conn: conn.close() + +if __name__ == "__main__": + migrate() diff --git a/ARCHIVE_legacy_scripts/migrate_opener_secondary.py b/ARCHIVE_legacy_scripts/migrate_opener_secondary.py new file mode 100644 index 00000000..90bbd585 --- /dev/null +++ b/ARCHIVE_legacy_scripts/migrate_opener_secondary.py @@ -0,0 +1,29 @@ +import sqlite3 +import sys + +DB_PATH = "/app/companies_v3_fixed_2.db" + +def migrate(): + try: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + print(f"Checking schema in {DB_PATH}...") + cursor.execute("PRAGMA table_info(companies)") + columns = [row[1] for row in cursor.fetchall()] + + if "ai_opener_secondary" in columns: + print("Column 'ai_opener_secondary' already exists. Skipping.") + else: + print("Adding column 'ai_opener_secondary' to 'companies' table...") + cursor.execute("ALTER TABLE companies ADD COLUMN ai_opener_secondary TEXT") + conn.commit() + print("✅ Migration successful.") + + except Exception as e: + print(f"❌ Migration failed: {e}") + finally: + if conn: conn.close() + +if __name__ == "__main__": + migrate() diff --git a/ARCHIVE_legacy_scripts/migrate_personas_v2.py b/ARCHIVE_legacy_scripts/migrate_personas_v2.py new file mode 100644 index 00000000..c0906eec --- /dev/null +++ b/ARCHIVE_legacy_scripts/migrate_personas_v2.py @@ -0,0 +1,30 @@ +import sqlite3 +import os + +DB_PATH = "/app/companies_v3_fixed_2.db" + +def migrate_personas(): + print(f"Adding new columns to 'personas' table in {DB_PATH}...") + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + columns_to_add = [ + ("description", "TEXT"), + ("convincing_arguments", "TEXT"), + ("typical_positions", "TEXT"), + ("kpis", "TEXT") + ] + + for col_name, col_type in columns_to_add: + try: + cursor.execute(f"ALTER TABLE personas ADD COLUMN {col_name} {col_type}") + print(f" Added column: {col_name}") + except sqlite3.OperationalError: + print(f" Column {col_name} already exists.") + + conn.commit() + conn.close() + print("Migration complete.") + +if __name__ == "__main__": + migrate_personas() diff --git a/old_brancheneinstufung.py b/ARCHIVE_legacy_scripts/old_brancheneinstufung.py similarity index 100% rename from old_brancheneinstufung.py rename to ARCHIVE_legacy_scripts/old_brancheneinstufung.py diff --git a/read_file_content.py b/ARCHIVE_legacy_scripts/read_file_content.py similarity index 100% rename from read_file_content.py rename to ARCHIVE_legacy_scripts/read_file_content.py diff --git a/ARCHIVE_legacy_scripts/read_matrix_entry.py b/ARCHIVE_legacy_scripts/read_matrix_entry.py new file mode 100644 index 00000000..28a62303 --- /dev/null +++ b/ARCHIVE_legacy_scripts/read_matrix_entry.py @@ -0,0 +1,37 @@ + +import sys +import os +sys.path.append(os.path.join(os.path.dirname(__file__), "company-explorer")) +from backend.database import SessionLocal, Industry, Persona, MarketingMatrix + +def read_specific_entry(industry_name: str, persona_name: str): + db = SessionLocal() + try: + entry = ( + db.query(MarketingMatrix) + .join(Industry) + .join(Persona) + .filter(Industry.name == industry_name, Persona.name == persona_name) + .first() + ) + + if not entry: + print(f"No entry found for {industry_name} and {persona_name}") + return + + print("--- Generated Text ---") + print(f"Industry: {industry_name}") + print(f"Persona: {persona_name}") + print("\n[Intro]") + print(entry.intro) + print("\n[Social Proof]") + print(entry.social_proof) + print("----------------------") + + finally: + db.close() + +if __name__ == "__main__": + read_specific_entry("Healthcare - Hospital", "Infrastruktur-Verantwortlicher") + + diff --git a/reindent.py b/ARCHIVE_legacy_scripts/reindent.py similarity index 100% rename from reindent.py rename to ARCHIVE_legacy_scripts/reindent.py diff --git a/ARCHIVE_legacy_scripts/standalone_importer.py b/ARCHIVE_legacy_scripts/standalone_importer.py new file mode 100644 index 00000000..a6708911 --- /dev/null +++ b/ARCHIVE_legacy_scripts/standalone_importer.py @@ -0,0 +1,92 @@ +import csv +from collections import Counter +import os +import argparse +from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime +from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.declarative import declarative_base +from datetime import datetime +import logging + +# --- Standalone Configuration --- +DATABASE_URL = "sqlite:////app/companies_v3_fixed_2.db" +LOG_FILE = "/app/Log_from_docker/standalone_importer.log" + +# --- Logging Setup --- +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(LOG_FILE), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# --- SQLAlchemy Models (simplified, only what's needed) --- +Base = declarative_base() + +class RawJobTitle(Base): + __tablename__ = 'raw_job_titles' + id = Column(Integer, primary_key=True) + title = Column(String, unique=True, index=True) + count = Column(Integer, default=1) + source = Column(String, default="import") + is_mapped = Column(Boolean, default=False) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + +# --- Database Connection --- +engine = create_engine(DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +def import_job_titles_standalone(file_path: str): + db = SessionLocal() + try: + logger.info(f"Starting standalone import of job titles from {file_path}") + + job_title_counts = Counter() + total_rows = 0 + + with open(file_path, 'r', encoding='utf-8') as f: + reader = csv.reader(f) + for row in reader: + if row and row[0].strip(): + title = row[0].strip() + job_title_counts[title] += 1 + total_rows += 1 + + logger.info(f"Read {total_rows} total job title entries. Found {len(job_title_counts)} unique titles.") + + added_count = 0 + updated_count = 0 + + for title, count in job_title_counts.items(): + existing_title = db.query(RawJobTitle).filter(RawJobTitle.title == title).first() + if existing_title: + if existing_title.count != count: + existing_title.count = count + updated_count += 1 + else: + new_title = RawJobTitle(title=title, count=count, source="csv_import", is_mapped=False) + db.add(new_title) + added_count += 1 + + db.commit() + logger.info(f"Standalone import complete. Added {added_count} new unique titles, updated {updated_count} existing titles.") + + except Exception as e: + logger.error(f"Error during standalone job title import: {e}", exc_info=True) + db.rollback() + finally: + db.close() + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Standalone script to import job titles from a CSV file.") + parser.add_argument("file_path", type=str, help="Path to the CSV file containing job titles.") + args = parser.parse_args() + + # Ensure the log directory exists + os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True) + + import_job_titles_standalone(args.file_path) diff --git a/ARCHIVE_legacy_scripts/test_api_logic.py b/ARCHIVE_legacy_scripts/test_api_logic.py new file mode 100644 index 00000000..c94d3a9b --- /dev/null +++ b/ARCHIVE_legacy_scripts/test_api_logic.py @@ -0,0 +1,22 @@ +import os +import sys + +# Add the company-explorer directory to the Python path +sys.path.append(os.path.abspath(os.path.join(os.getcwd(), 'company-explorer'))) + +from backend.database import SessionLocal, MarketingMatrix, Industry, Persona +from sqlalchemy.orm import joinedload + +db = SessionLocal() +try: + query = db.query(MarketingMatrix).options( + joinedload(MarketingMatrix.industry), + joinedload(MarketingMatrix.persona) + ) + entries = query.all() + print(f"Total entries: {len(entries)}") + for e in entries[:3]: + print(f"ID={e.id}, Industry={e.industry.name if e.industry else 'N/A'}, Persona={e.persona.name if e.persona else 'N/A'}") + print(f" Subject: {e.subject}") +finally: + db.close() diff --git a/test_company_explorer_connector.py b/ARCHIVE_legacy_scripts/test_company_explorer_connector.py similarity index 100% rename from test_company_explorer_connector.py rename to ARCHIVE_legacy_scripts/test_company_explorer_connector.py diff --git a/test_core_functionality.py b/ARCHIVE_legacy_scripts/test_core_functionality.py similarity index 100% rename from test_core_functionality.py rename to ARCHIVE_legacy_scripts/test_core_functionality.py diff --git a/test_explorer_connection.py b/ARCHIVE_legacy_scripts/test_explorer_connection.py similarity index 100% rename from test_explorer_connection.py rename to ARCHIVE_legacy_scripts/test_explorer_connection.py diff --git a/test_export.py b/ARCHIVE_legacy_scripts/test_export.py similarity index 100% rename from test_export.py rename to ARCHIVE_legacy_scripts/test_export.py diff --git a/ARCHIVE_legacy_scripts/test_opener_api.py b/ARCHIVE_legacy_scripts/test_opener_api.py new file mode 100644 index 00000000..3c351e57 --- /dev/null +++ b/ARCHIVE_legacy_scripts/test_opener_api.py @@ -0,0 +1,91 @@ +import requests +import os +import sys +import time + +# Load credentials from .env +# Simple manual parser to avoid dependency on python-dotenv +def load_env(path): + if not os.path.exists(path): + print(f"Warning: .env file not found at {path}") + return + with open(path) as f: + for line in f: + if line.strip() and not line.startswith('#'): + key, val = line.strip().split('=', 1) + os.environ.setdefault(key, val) + +load_env('/app/.env') + +API_USER = os.getenv("API_USER", "admin") +API_PASS = os.getenv("API_PASSWORD", "gemini") +CE_URL = "http://127.0.0.1:8000" # Target the local container (assuming port 8000 is mapped) +TEST_CONTACT_ID = 1 # Therme Erding + +def run_test(): + print("🚀 STARTING API-LEVEL E2E TEXT GENERATION TEST\n") + + # --- Health Check --- + print("Waiting for Company Explorer API to be ready...") + for i in range(10): + try: + health_resp = requests.get(f"{CE_URL}/api/health", auth=(API_USER, API_PASS), timeout=2) + if health_resp.status_code == 200: + print("✅ API is ready.") + break + except requests.exceptions.RequestException: + pass + if i == 9: + print("❌ API not ready after 20 seconds. Aborting.") + return False + time.sleep(2) + + scenarios = [ + {"name": "Infrastructure Role", "job_title": "Facility Manager", "opener_field": "opener", "keyword": "Sicherheit"}, + {"name": "Operational Role", "job_title": "Leiter Badbetrieb", "opener_field": "opener_secondary", "keyword": "Gäste"} + ] + + all_passed = True + for s in scenarios: + print(f"--- Testing: {s['name']} ---") + endpoint = f"{CE_URL}/api/provision/superoffice-contact" + payload = { + "so_contact_id": TEST_CONTACT_ID, + "job_title": s['job_title'] + } + + try: + resp = requests.post(endpoint, json=payload, auth=(API_USER, API_PASS)) + resp.raise_for_status() + data = resp.json() + + # --- Assertions --- + opener = data.get('opener') + opener_sec = data.get('opener_secondary') + + assert opener, "❌ FAIL: Primary opener is missing!" + print(f" ✅ Primary Opener: '{opener}'") + + assert opener_sec, "❌ FAIL: Secondary opener is missing!" + print(f" ✅ Secondary Opener: '{opener_sec}'") + + target_opener_text = data.get(s['opener_field']) + assert s['keyword'].lower() in target_opener_text.lower(), f"❌ FAIL: Keyword '{s['keyword']}' not in '{s['opener_field']}'!" + print(f" ✅ Keyword '{s['keyword']}' found in correct opener.") + + print(f"--- ✅ PASSED: {s['name']} ---\\n") + + except Exception as e: + print(f" ❌ TEST FAILED: {e}") + if hasattr(e, 'response') and e.response is not None: + print(f" Response: {e.response.text}") + all_passed = False + + return all_passed + +if __name__ == "__main__": + if run_test(): + print("🏁 All scenarios passed successfully!") + else: + print("🔥 Some scenarios failed.") + sys.exit(1) diff --git a/test_parser.py b/ARCHIVE_legacy_scripts/test_parser.py similarity index 100% rename from test_parser.py rename to ARCHIVE_legacy_scripts/test_parser.py diff --git a/ARCHIVE_legacy_scripts/test_provisioning_api.py b/ARCHIVE_legacy_scripts/test_provisioning_api.py new file mode 100644 index 00000000..b2d179ef --- /dev/null +++ b/ARCHIVE_legacy_scripts/test_provisioning_api.py @@ -0,0 +1,12 @@ +import requests +import json + +url = "http://company-explorer:8000/api/provision/superoffice-contact" +payload = {"so_contact_id": 4} +auth = ("admin", "gemini") + +try: + resp = requests.post(url, json=payload, auth=auth) + print(json.dumps(resp.json(), indent=2)) +except Exception as e: + print(f"Error: {e}") diff --git a/test_pytube.py b/ARCHIVE_legacy_scripts/test_pytube.py similarity index 100% rename from test_pytube.py rename to ARCHIVE_legacy_scripts/test_pytube.py diff --git a/test_selenium.py b/ARCHIVE_legacy_scripts/test_selenium.py similarity index 100% rename from test_selenium.py rename to ARCHIVE_legacy_scripts/test_selenium.py diff --git a/trading_twins_tool.py b/ARCHIVE_legacy_scripts/trading_twins_tool.py similarity index 60% rename from trading_twins_tool.py rename to ARCHIVE_legacy_scripts/trading_twins_tool.py index 773cc18b..1f4df58a 100644 --- a/trading_twins_tool.py +++ b/ARCHIVE_legacy_scripts/trading_twins_tool.py @@ -1,6 +1,16 @@ import json import time import os +import sys + +# Ensure we can import from lead-engine +sys.path.append(os.path.join(os.path.dirname(__file__), 'lead-engine')) +try: + from trading_twins_ingest import process_leads +except ImportError: + print("Warning: Could not import trading_twins_ingest from lead-engine. Email ingestion disabled.") + process_leads = None + from company_explorer_connector import handle_company_workflow def run_trading_twins_process(target_company_name: str): @@ -46,6 +56,14 @@ def run_trading_twins_process(target_company_name: str): print(f"Trading Twins Analyse für {target_company_name} abgeschlossen.") print(f"{'='*50}\n") +def run_email_ingest(): + """Starts the automated email ingestion process for Tradingtwins leads.""" + if process_leads: + print("\nStarting automated email ingestion via Microsoft Graph...") + process_leads() + print("Email ingestion completed.") + else: + print("Error: Email ingestion module not available.") if __name__ == "__main__": # Simulieren der Umgebungsvariablen für diesen Testlauf, falls nicht gesetzt @@ -54,26 +72,28 @@ if __name__ == "__main__": if "COMPANY_EXPLORER_API_PASSWORD" not in os.environ: os.environ["COMPANY_EXPLORER_API_PASSWORD"] = "gemini" - # Testfall 1: Ein Unternehmen, das wahrscheinlich bereits existiert - # Da 'Robo-Planet GmbH' bei den vorherigen Läufen erstellt wurde, sollte es jetzt gefunden werden. - run_trading_twins_process("Robo-Planet GmbH") - - # Kurze Pause zwischen den Testläufen - time.sleep(5) - - # Testfall 1b: Ein bekanntes, real existierendes Unternehmen - run_trading_twins_process("Klinikum Landkreis Erding") - - # Kurze Pause zwischen den Testläufen - time.sleep(5) - - # Testfall 2: Ein neues, eindeutiges Unternehmen - new_unique_company_name = f"Trading Twins New Target {int(time.time())}" - run_trading_twins_process(new_unique_company_name) - - # Kurze Pause - time.sleep(5) + print("Trading Twins Tool - Main Menu") + print("1. Process specific company name") + print("2. Ingest leads from Email (info@robo-planet.de)") + print("3. Run demo sequence (Robo-Planet, Erding, etc.)") - # Testfall 3: Ein weiteres neues Unternehmen, um die Erstellung zu prüfen - another_new_company_name = f"Another Demo Corp {int(time.time())}" - run_trading_twins_process(another_new_company_name) + choice = input("\nSelect option (1-3): ").strip() + + if choice == "1": + name = input("Enter company name: ").strip() + if name: + run_trading_twins_process(name) + elif choice == "2": + run_email_ingest() + elif choice == "3": + # Testfall 1: Ein Unternehmen, das wahrscheinlich bereits existiert + run_trading_twins_process("Robo-Planet GmbH") + time.sleep(2) + # Testfall 1b: Ein bekanntes, real existierendes Unternehmen + run_trading_twins_process("Klinikum Landkreis Erding") + time.sleep(2) + # Testfall 2: Ein neues, eindeutiges Unternehmen + new_unique_company_name = f"Trading Twins New Target {int(time.time())}" + run_trading_twins_process(new_unique_company_name) + else: + print("Invalid choice.") diff --git a/train_model.py b/ARCHIVE_legacy_scripts/train_model.py similarity index 100% rename from train_model.py rename to ARCHIVE_legacy_scripts/train_model.py diff --git a/ARCHIVE_legacy_scripts/trigger_resync.py b/ARCHIVE_legacy_scripts/trigger_resync.py new file mode 100644 index 00000000..9af068cd --- /dev/null +++ b/ARCHIVE_legacy_scripts/trigger_resync.py @@ -0,0 +1,25 @@ +import sqlite3 +import json +import time + +DB_PATH = "connector_queue.db" + +def trigger_resync(contact_id): + print(f"🚀 Triggering manual resync for Contact {contact_id}...") + + payload = { + "Event": "contact.changed", + "PrimaryKey": contact_id, + "ContactId": contact_id, + "Changes": ["UserDefinedFields", "Name"] # Dummy changes to pass filters + } + + with sqlite3.connect(DB_PATH) as conn: + conn.execute( + "INSERT INTO jobs (event_type, payload, status) VALUES (?, ?, ?)", + ("contact.changed", json.dumps(payload), 'PENDING') + ) + print("✅ Job added to queue.") + +if __name__ == "__main__": + trigger_resync(6) # Bennis Playland has CRM ID 6 diff --git a/ARCHIVE_legacy_scripts/verify_db.py b/ARCHIVE_legacy_scripts/verify_db.py new file mode 100644 index 00000000..975f483e --- /dev/null +++ b/ARCHIVE_legacy_scripts/verify_db.py @@ -0,0 +1,13 @@ +import sqlite3 + +DB_PATH = "/app/companies_v3_fixed_2.db" +conn = sqlite3.connect(DB_PATH) +cursor = conn.cursor() +cursor.execute("SELECT name, description, convincing_arguments FROM personas") +rows = cursor.fetchall() +for row in rows: + print(f"Persona: {row[0]}") + print(f" Description: {row[1][:100]}...") + print(f" Convincing: {row[2][:100]}...") + print("-" * 20) +conn.close() diff --git a/ARCHIVE_vor_migration/Fotograf.de/README.md b/ARCHIVE_vor_migration/Fotograf.de/README.md new file mode 100644 index 00000000..601c3d1c --- /dev/null +++ b/ARCHIVE_vor_migration/Fotograf.de/README.md @@ -0,0 +1,75 @@ +# Archivierte Fotograf.de Tools + +Dieses Verzeichnis (`ARCHIVE_vor_migration/Fotograf.de/`) enthält zwei archivierte Tools, die zuvor für die Interaktion mit `app.fotograf.de` und die Erstellung von Google Docs Teilnehmerlisten verwendet wurden. + +Beide Tools sind hier isoliert und dokumentiert, um eine spätere Wiederverwendung und Überarbeitung zu erleichtern. + +## 1. Fotograf.de Scraper + +**Verzeichnis:** `./scraper/` + +**Zweck:** +Ein Python-basiertes Skript, das die Website `app.fotograf.de` automatisiert besucht, sich anmeldet und in zwei Modi Daten extrahiert: +1. **E-Mail-Liste erstellen:** Sammelt Kontaktdaten (Käufer, E-Mail, Kindnamen, Login-URLs) und speichert sie in einer CSV-Datei (`supermailer_fertige_liste.csv`). +2. **Statistik auswerten:** Erstellt eine Statistik-CSV-Datei (`job_statistik.csv`) über Album-Käufe. + +**Benötigte Dateien:** +* `./scraper/scrape_fotograf.py`: Das Hauptskript mit der gesamten Logik. +* `./scraper/fotograf_credentials.json`: **(Manuell zu erstellen!)** Diese Datei muss Ihre Login-Daten für `app.fotograf.de` im folgenden JSON-Format enthalten: + ```json + { + "PROFILNAME": { + "username": "IHR_BENUTZERNAME", + "password": "IHR_PASSWORT" + } + } + ``` + +**Ausführung über Docker:** +Das Tool wird in einer Docker-Umgebung ausgeführt, die Google Chrome und Selenium bereitstellt. + +1. **Image bauen (einmalig oder bei Änderungen an Dockerfile/requirements.txt):** + Navigieren Sie zum Root-Verzeichnis des Hauptprojekts (`/app`) und verwenden Sie das dortige `Dockerfile.brancheneinstufung`: + ```bash + cd /app + docker build -f Dockerfile.brancheneinstufung -t fotograf-scraper . + ``` + *(Hinweis: Das `Dockerfile.brancheneinstufung` verwendet die globale `requirements.txt` im Root-Verzeichnis, welche `selenium` enthält.)* + +2. **Container starten und Skript ausführen:** + Vom Root-Verzeichnis des Hauptprojekts (`/app`) aus: + ```bash + cd /app + docker run -it --rm -v "$(pwd):/app" fotograf-scraper python3 /app/ARCHIVE_vor_migration/Fotograf.de/scraper/scrape_fotograf.py + ``` + Das Skript fragt Sie interaktiv nach dem gewünschten Modus und der URL des Fotoauftrags. + + +## 2. Google Docs Teilnehmerlisten-Generator + +**Verzeichnis:** `./list_generator/` + +**Zweck:** +Ein Python-Skript, das CSV-Dateien einliest und daraus formatierte Teilnehmerlisten als neues Google Docs-Dokument im Google Drive erstellt. Das Tool ist interaktiv und fragt beim Start Details wie den Namen der Veranstaltung, den Einrichtungstyp und den Ausgabemodus ab. + +**Benötigte Dateien:** +* `./list_generator/list_generator.py`: Das Hauptskript mit der gesamten Logik. +* `./list_generator/Namensliste.csv`: **(Manuell zu erstellen!)** Eine CSV-Datei mit den Anmeldungen für Kindergärten/Schulen. +* `./list_generator/familien.csv`: **(Manuell zu erstellen!)** Eine CSV-Datei mit den Anmeldungen für Familien-Shootings. +* `./list_generator/service_account.json`: **(Manuell zu erstellen!)** Die JSON-Datei mit den Anmeldeinformationen für den Google Cloud Service Account. Diese Datei wird benötigt, um auf Google Docs und Google Drive zuzugreifen. + +**Ausführung:** +Navigieren Sie in das Verzeichnis des Tools und starten Sie es mit Python: +```bash +cd /app/ARCHIVE_vor_migration/Fotograf.de/list_generator/ +python3 list_generator.py +``` +*(Stellen Sie sicher, dass alle benötigten Python-Bibliotheken wie `google-api-python-client` etc. in Ihrer Umgebung installiert sind. Diese sind vermutlich über die globale `requirements.txt` im Root-Verzeichnis des Hauptprojekts verfügbar.)* + +## Wichtiger Hinweis zu Credentials (Sicherheit) + +Die Tools verwenden `fotograf_credentials.json` und `service_account.json` zur Authentifizierung. Diese Dateien enthalten sensitive Zugangsdaten und wurden **bewusst aus der Git-Historie entfernt** und nicht im Repository abgelegt. + +**Für die Wiederinbetriebnahme müssen diese Dateien manuell im jeweiligen Tool-Verzeichnis (`./scraper/` bzw. `./list_generator/`) erstellt und mit den korrekten Zugangsdaten befüllt werden.** + +**Priorität für die Überarbeitung:** Bei einer zukünftigen Überarbeitung dieser Tools ist es **zwingend erforderlich**, die Handhabung der Credentials zu verbessern. Statt fester JSON-Dateien sollten Umgebungsvariablen (`.env`) oder ein sicherer Secret Management Service verwendet werden, um die Sicherheitsstandards zu erhöhen. diff --git a/ARCHIVE_vor_migration/Fotograf.de/list_generator/README.md b/ARCHIVE_vor_migration/Fotograf.de/list_generator/README.md new file mode 100644 index 00000000..c00093a4 --- /dev/null +++ b/ARCHIVE_vor_migration/Fotograf.de/list_generator/README.md @@ -0,0 +1,22 @@ +# Google Docs Teilnehmerlisten-Generator (Archiviert) + +Dieses Verzeichnis enthält die archivierten Dateien für den "Google Docs Teilnehmerlisten-Generator". + +**Zweck:** +Ein Python-Skript, das CSV-Dateien einliest und daraus formatierte Teilnehmerlisten als neues Google Docs-Dokument im Google Drive erstellt. Das Tool ist interaktiv und fragt beim Start Details wie den Namen der Veranstaltung ab. + +**Zugehörige Dateien in diesem Ordner:** +* `list_generator.py`: Das Hauptskript mit der gesamten Logik. + +**Manuell zu erstellende Dateien:** +Diese Dateien werden vom Skript als Input benötigt und müssen im selben Verzeichnis liegen: +* `Namensliste.csv`: Eine CSV-Datei mit den Anmeldungen für Kindergärten/Schulen. +* `familien.csv`: Eine CSV-Datei mit den Anmeldungen für Familien-Shootings. +* `service_account.json`: Die JSON-Datei mit den Anmeldeinformationen für den Google Cloud Service Account, der die Berechtigung hat, auf Google Docs und Google Drive zuzugreifen. + +**Hinweis zur Ausführung:** +Das Skript wird direkt mit Python ausgeführt, z.B.: +```bash +python3 list_generator.py +``` +Stellen Sie sicher, dass alle benötigten Bibliotheken (wie `google-api-python-client`, `google-auth-httplib2`, `google-auth-oauthlib`) in Ihrer Python-Umgebung installiert sind. Diese sind vermutlich in der globalen `requirements.txt` im Root-Verzeichnis des Projekts enthalten. diff --git a/list_generator.py b/ARCHIVE_vor_migration/Fotograf.de/list_generator/list_generator.py similarity index 100% rename from list_generator.py rename to ARCHIVE_vor_migration/Fotograf.de/list_generator/list_generator.py diff --git a/ARCHIVE_vor_migration/Fotograf.de/scraper/README.md b/ARCHIVE_vor_migration/Fotograf.de/scraper/README.md new file mode 100644 index 00000000..11c56ea4 --- /dev/null +++ b/ARCHIVE_vor_migration/Fotograf.de/scraper/README.md @@ -0,0 +1,32 @@ +# Fotograf.de Scraper (Archiviert) + +Dieses Verzeichnis enthält die archivierten Dateien für den "Fotograf.de Scraper". + +**Zweck:** +Ein Python-basiertes Tool, das die Website `app.fotograf.de` automatisiert besucht, sich anmeldet und in zwei Modi Daten extrahiert: +1. **E-Mail-Liste erstellen:** Sammelt Kontaktdaten und speichert sie in einer CSV-Datei (`supermailer_fertige_liste.csv`). +2. **Statistik auswerten:** Erstellt eine Statistik-CSV-Datei (`job_statistik.csv`). + +**Zugehörige Dateien in diesem Ordner:** +* `scrape_fotograf.py`: Das Hauptskript mit der gesamten Logik. + +**Manuell zu erstellende Dateien:** +* `fotograf_credentials.json`: Diese Datei wird vom Skript benötigt und muss die Login-Daten für `app.fotograf.de` im folgenden JSON-Format enthalten: + ```json + { + "PROFILNAME": { + "username": "IHR_BENUTZERNAME", + "password": "IHR_PASSWORT" + } + } + ``` + +**Externe Abhängigkeiten (befinden sich im Hauptverzeichnis des Projekts):** +* **Dockerfile:** `Dockerfile.brancheneinstufung` wurde wahrscheinlich verwendet, um ein Docker-Image für dieses Tool zu erstellen. Es installiert Google Chrome und die notwendigen Python-Pakete. +* **Python-Abhängigkeiten:** Die globale `requirements.txt` im Root-Verzeichnis enthält `selenium` und andere benötigte Bibliotheken. + +**Beispielhafter `docker run`-Befehl:** +1. Bauen Sie das Image (nur einmalig): `docker build -f Dockerfile.brancheneinstufung -t fotograf-scraper .` +2. Führen Sie den Container aus: `docker run -it --rm -v "$(pwd):/app" fotograf-scraper python3 /app/ARCHIVE_vor_migration/Fotograf.de/scraper/scrape_fotograf.py` + +(Pfade müssen ggf. angepasst werden, je nachdem, von wo der Befehl ausgeführt wird.) diff --git a/scrape_fotograf.py b/ARCHIVE_vor_migration/Fotograf.de/scraper/scrape_fotograf.py similarity index 100% rename from scrape_fotograf.py rename to ARCHIVE_vor_migration/Fotograf.de/scraper/scrape_fotograf.py diff --git a/ARCHIVE_vor_migration/Generating b/ARCHIVE_vor_migration/Generating new file mode 100644 index 00000000..e69de29b diff --git a/google_sheet_handler.txt b/ARCHIVE_vor_migration/google_sheet_handler.txt similarity index 100% rename from google_sheet_handler.txt rename to ARCHIVE_vor_migration/google_sheet_handler.txt diff --git a/FRITZbox7530.pdf b/FRITZbox7530.pdf deleted file mode 100644 index 16b97799..00000000 Binary files a/FRITZbox7530.pdf and /dev/null differ diff --git a/GEMINI.md b/GEMINI.md index 514a8a29..507acc39 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -20,6 +20,41 @@ Dies ist in der Vergangenheit mehrfach passiert und hat zu massivem Datenverlust - **Git-Repository:** Dieses Projekt wird über ein Git-Repository verwaltet. Alle Änderungen am Code werden versioniert. Beachten Sie den Abschnitt "Git Workflow & Conventions" für unsere Arbeitsregeln. - **WICHTIG:** Der AI-Agent kann Änderungen committen, aber aus Sicherheitsgründen oft nicht `git push` ausführen. Bitte führen Sie `git push` manuell aus, wenn der Agent dies meldet. +--- +## ‼️ Aktueller Projekt-Fokus (März 2026): Migration & Stabilisierung + +**Das System wurde am 07. März 2026 vollständig stabilisiert und für den Umzug auf die Ubuntu VM (`docker1`) vorbereitet.** + +Alle aktuellen Aufgaben für den Umzug sind hier zentralisiert: +➡️ **[`RELOCATION.md`](./RELOCATION.md)** + +--- + +## ✅ Current Status (March 7, 2026) - STABLE + +Das System läuft stabil auf der Synology-Entwicklungsumgebung. + +### 1. SuperOffice Connector (v2.1.1 - "Echo Shield") +* **Echo-Prävention (Härtung):** Der Worker (`worker.py`) identifiziert sich beim Start dynamisch (`/Associate/Me`) und ignoriert strikt alle Events, die vom eigenen User (z.B. ID 528) ausgelöst wurden. +* **Feld-Filter:** Änderungen werden nur verarbeitet, wenn relevante Felder (Name, URL, JobTitle) betroffen sind. Irrelevante Updates (z.B. `lastUpdated`) werden geskippt. +* **Webhook:** Registriert auf `https://floke-ai.duckdns.org/connector/webhook` mit Token-Validierung im Query-String. + +### 2. Company Explorer (v0.7.4) +* **Datenbank:** Schema repariert (`fix_missing_columns.py` ausgeführt). Fehlende Spalten (`street`, `zip_code`, `unsubscribe_token`) sind nun vorhanden. +* **Frontend:** Build-Pipeline repariert. PostCSS/Tailwind generieren jetzt wieder korrektes Styling. +* **Persistence:** Datenbank liegt sicher im Docker Volume `explorer_db_data`. + +### 3. Lead Engine (Trading Twins) +* **Integration:** In `docker-compose.yml` integriert und unter `/lead/` via Gateway erreichbar. +* **Persistence:** Nutzt Volume `lead_engine_data`. +* **Status:** UI läuft. E-Mail-Ingest via MS Graph benötigt noch Credentials. + +### 4. Infrastructure +* **Secrets:** Alle API-Keys (OpenAI, Gemini, SO, DuckDNS) sind zentral in der `.env` Datei. +* **DuckDNS:** Service läuft und aktualisiert die IP erfolgreich. + +--- + ## Git Workflow & Conventions ### Den Arbeitstag abschließen mit `#fertig` @@ -37,6 +72,38 @@ Wenn Sie `#fertig` eingeben, führt der Agent folgende Schritte aus: - Der Status des Tasks in Notion wird auf "Done" (oder einen anderen passenden Status) gesetzt. 4. **Commit & Push:** Wenn Code-Änderungen vorhanden sind, wird ein Commit erstellt und ein `git push` interaktiv angefragt. +### ⚠️ Troubleshooting: Git `push`/`pull` Fehler in Docker-Containern + +Gelegentlich kann es vorkommen, dass `git push` oder `git pull` Befehle aus dem `gemini-session` Docker-Container heraus mit Fehlern wie `Could not resolve host` oder `Failed to connect to ` fehlschlagen, selbst wenn die externe Gitea-URL (z.B. `floke-gitea.duckdns.org`) im Host-System erreichbar ist. Dies liegt daran, dass der Docker-Container möglicherweise nicht dieselben DNS-Auflösungsmechanismen oder eine direkte Verbindung zur externen Adresse hat. + +**Problem:** Standard-DNS-Auflösung und externe Hostnamen schlagen innerhalb des Docker-Containers fehl. + +**Lösung:** Um eine robuste und direkte Verbindung zum Gitea-Container auf dem *selben Docker-Host* herzustellen, sollte die Git Remote URL auf die **lokale IP-Adresse des Docker-Hosts** und die **token-basierte Authentifizierung** umgestellt werden. + +**Schritte zur Konfiguration:** + +1. **Lokale IP des Docker-Hosts ermitteln:** + * Finden Sie die lokale IP-Adresse des Servers (z.B. Ihrer Diskstation), auf dem die Docker-Container laufen. Beispiel: `192.168.178.6`. +2. **Gitea-Token aus `.env` ermitteln:** + * Finden Sie das Gitea-Token (das im Format `` in der `.env`-Datei oder in der vorherigen `git remote -v` Ausgabe zu finden ist). Beispiel: `318c736205934dd066b6bbcb1d732931eaa7c8c4`. +3. **Git Remote URL aktualisieren:** + * Verwenden Sie den folgenden Befehl, um die Remote-URL zu aktualisieren. Ersetzen Sie ``, `` und `` durch Ihre Werte. + ```bash + git remote set-url origin http://:@:3000/Floke/Brancheneinstufung2.git + ``` + * **Beispiel (mit Ihren Daten):** + ```bash + git remote set-url origin http://Floke:318c736205934dd066b6bbcb1d732931eaa7c8c4@192.168.178.6:3000/Floke/Brancheneinstufung2.git + ``` + *(Hinweis: Für die interne Docker-Kommunikation ist `http` anstelle von `https` oft ausreichend und kann Probleme mit SSL-Zertifikaten vermeiden.)* +4. **Verifizierung:** + * Führen Sie `git fetch` aus, um die neue Konfiguration zu testen. Es sollte nun ohne Passwortabfrage funktionieren: + ```bash + git fetch + ``` + +Diese Konfiguration gewährleistet eine stabile Git-Verbindung innerhalb Ihrer Docker-Umgebung. + ## Project Overview @@ -105,6 +172,17 @@ The system architecture has evolved from a CLI-based toolset to a modern web app * **Problem:** Users didn't see when a background job finished. * **Solution:** Implementing a polling mechanism (`setInterval`) tied to a `isProcessing` state is superior to static timeouts for long-running AI tasks. +7. **Hyper-Personalized Marketing Engine (v3.2) - "Deep Persona Injection":** + * **Problem:** Marketing texts were too generic and didn't reflect the specific psychological or operative profile of the different target roles (e.g., CFO vs. Facility Manager). + * **Solution (Deep Sync & Prompt Hardening):** + 1. **Extended Schema:** Added `description`, `convincing_arguments`, and `kpis` to the `Persona` database model to store richer profile data. + 2. **Notion Master Sync:** Updated the synchronization logic to pull these deep insights directly from the Notion "Personas / Roles" database. + 3. **Role-Centric Prompts:** The `MarketingMatrix` generator was re-engineered to inject the persona's "Mindset" and "KPIs" into the prompt. + * **Example (Healthcare):** + - **Infrastructure Lead:** Focuses now on "IT Security", "DSGVO Compliance", and "WLAN integration". + - **Economic Buyer (CFO):** Focuses on "ROI Amortization", "Reduction of Overtime", and "Flexible Financing (RaaS)". + * **Verification:** Verified that the transition from a company-specific **Opener** (e.g., observing staff shortages at Klinikum Erding) to the **Role-specific Intro** (e.g., pitching transport robots to reduce walking distances for nursing directors) is seamless and logical. + ## Metric Parser - Regression Tests To ensure the stability and accuracy of the metric extraction logic, a dedicated test suite (`/company-explorer/backend/tests/test_metric_parser.py`) has been created. It covers the following critical, real-world bug fixes: @@ -122,8 +200,185 @@ To ensure the stability and accuracy of the metric extraction logic, a dedicated These tests are crucial for preventing regressions as the parser logic evolves. -## Next Steps -* **Marketing Automation:** Implement the actual sending logic (or export) based on the contact status. -* **Job Role Mapping Engine:** Connect the configured patterns to the contact import/creation process to auto-assign roles. -* **Industry Classification Engine:** Connect the configured industries to the AI Analysis prompt to enforce the "Strict Mode" mapping. -* **Export:** Generate Excel/CSV enriched reports (already partially implemented via JSON export). +## Notion Maintenance & Data Sync + +Since the "Golden Record" for Industry Verticals (Pains, Gains, Products) resides in Notion, specific tools are available to read and sync this data. + +**Location:** `/app/company-explorer/backend/scripts/notion_maintenance/` + +**Prerequisites:** +- Ensure `.env` is loaded with `NOTION_API_KEY` and correct DB IDs. + +**Key Scripts:** + +1. **`check_relations.py` (Reader - Deep):** + - **Purpose:** Reads Verticals and resolves linked Product Categories (Relation IDs -> Names). Essential for verifying the "Primary/Secondary Product" logic. + - **Usage:** `python3 check_relations.py` + +2. **`update_notion_full.py` (Writer - Batch):** + - **Purpose:** Batch updates Pains and Gains for multiple verticals. Use this as a template when refining the messaging strategy. + - **Usage:** Edit the dictionary in the script, then run `python3 update_notion_full.py`. + +3. **`list_notion_structure.py` (Schema Discovery):** + - **Purpose:** Lists all property keys and page titles. Use this to debug schema changes (e.g. if a column was renamed). + - **Usage:** `python3 list_notion_structure.py` + + ## Next Steps (Updated Feb 27, 2026) + + ***HINWEIS:*** *Dieser Abschnitt ist veraltet. Die aktuellen nächsten Schritte beziehen sich auf die Migrations-Vorbereitung und sind in der Datei [`RELOCATION.md`](./RELOCATION.md) dokumentiert.* + + * **Notion Content:** Finalize "Pains" and "Gains" for all 25 verticals in the Notion master database. + * **Intelligence:** Run `generate_matrix.py` in the Company Explorer backend to populate the matrix for all new English vertical names. + * **Automation:** Register the production webhook (requires `admin-webhooks` rights) to enable real-time CRM sync without manual job injection. + * **Execution:** Connect the "Sending Engine" (the actual email dispatch logic) to the SuperOffice fields. + * **Monitoring:** Monitor the 'Atomic PATCH' logs in production for any 400 errors regarding field length or specific character sets. + + + ## Company Explorer Access & Debugging + + The **Company Explorer** is the central intelligence engine. + + **Core Paths:** + * **Database:** `/app/companies_v3_fixed_2.db` (SQLite) + * **Backend Code:** `/app/company-explorer/backend/` + * **Logs:** `/app/logs_debug/company_explorer_debug.log` + + **Accessing Data:** + To inspect live data without starting the full stack, use `sqlite3` directly or the helper scripts (if environment permits). + + * **Direct SQL:** `sqlite3 /app/companies_v3_fixed_2.db "SELECT * FROM companies WHERE name LIKE '%Firma%';" ` + * **Python (requires env):** The app runs in a Docker container. When debugging from outside (CLI agent), Python dependencies like `sqlalchemy` might be missing in the global scope. Prefer `sqlite3` for quick checks. + + **Key Endpoints (Internal API :8000):** + * `POST /api/provision/superoffice-contact`: Triggers the text generation logic. + * `GET /api/companies/{id}`: Full company profile including enrichment data. + + **Troubleshooting:** + * **"BaseModel" Error:** Usually a mix-up between Pydantic and SQLAlchemy `Base`. Check imports in `database.py`. + * **Missing Dependencies:** The CLI agent runs in `/app` but not necessarily inside the container's venv. Use standard tools (`grep`, `sqlite3`) where possible. + +--- + +## Critical Debugging Session (Feb 21, 2026) - Re-Stabilizing the Analysis Engine + +A critical session was required to fix a series of cascading failures in the `ClassificationService`. The key takeaways are documented here to prevent future issues. + +1. **The "Phantom" `NameError`:** + * **Symptom:** The application crashed with a `NameError: name 'joinedload' is not defined`, even though the import was correctly added to `classification.py`. + * **Root Cause:** The `uvicorn` server's hot-reload mechanism within the Docker container did not reliably pick up file changes made from outside the container. A simple `docker-compose restart` was insufficient to clear the process's cached state. + * **Solution:** After any significant code change, especially to imports or core logic, a forced-recreation of the container is **mandatory**. + ```bash + # Correct Way to Apply Changes: + docker-compose up -d --build --force-recreate company-explorer + ``` + +2. **The "Invisible" Logs:** + * **Symptom:** No debug logs were being written, making it impossible to trace the execution flow. + * **Root Cause:** The `LOG_DIR` path in `/company-explorer/backend/config.py` was misconfigured (`/app/logs_debug`) and did not point to the actual, historical log directory (`/app/Log_from_docker`). + * **Solution:** Configuration paths must be treated as absolute and verified. Correcting the `LOG_DIR` path immediately resolved the issue. + +3. **Inefficient Debugging Loop:** + * **Symptom:** The cycle of triggering a background job via API, waiting, and then manually checking logs was slow and inefficient. + * **Root Cause:** Lack of a tool to test the core application logic in isolation. + * **Solution:** The creation of a dedicated, interactive test script (`/company-explorer/backend/scripts/debug_single_company.py`). This script allows running the entire analysis for a single company in the foreground, providing immediate and detailed feedback. This pattern is invaluable for complex, multi-step processes and should be a standard for future development. +## Production Migration & Multi-Campaign Support (Feb 27, 2026) + +The system has been fully migrated to the SuperOffice production environment (`online3.superoffice.com`, tenant `Cust26720`). + +### 1. Final UDF Mappings (Production) +These ProgIDs are verified and active for the production tenant: + +| Field Purpose | Entity | ProgID | Notes | +| :--- | :--- | :--- | :--- | +| **MA Subject** | Person | `SuperOffice:19` | | +| **MA Intro** | Person | `SuperOffice:20` | | +| **MA Social Proof** | Person | `SuperOffice:21` | | +| **MA Unsubscribe** | Person | `SuperOffice:22` | URL format | +| **MA Campaign** | Person | `SuperOffice:23` | List field (uses `:DisplayText`) | +| **Vertical** | Contact | `SuperOffice:83` | List field (mapped via JSON) | +| **AI Summary** | Contact | `SuperOffice:84` | Truncated to 132 chars | +| **AI Last Update** | Contact | `SuperOffice:85` | Format: `[D:MM/DD/YYYY HH:MM:SS]` | +| **Opener Primary** | Contact | `SuperOffice:86` | | +| **Opener Secondary**| Contact | `SuperOffice:87` | | +| **Last Outreach** | Contact | `SuperOffice:88` | | + +### 2. Vertical ID Mapping (Production) +The full list of 25 verticals with their internal SuperOffice IDs (List `udlist331`): +`Automotive - Dealer: 1613, Corporate - Campus: 1614, Energy - Grid & Utilities: 1615, Energy - Solar/Wind: 1616, Healthcare - Care Home: 1617, Healthcare - Hospital: 1618, Hospitality - Gastronomy: 1619, Hospitality - Hotel: 1620, Industry - Manufacturing: 1621, Infrastructure - Communities: 1622, Infrastructure - Public: 1623, Infrastructure - Transport: 1624, Infrastructure - Parking: 1625, Leisure - Entertainment: 1626, Leisure - Fitness: 1627, Leisure - Indoor Active: 1628, Leisure - Outdoor Park: 1629, Leisure - Wet & Spa: 1630, Logistics - Warehouse: 1631, Others: 1632, Reinigungsdienstleister: 1633, Retail - Food: 1634, Retail - Non-Food: 1635, Retail - Shopping Center: 1636, Tech - Data Center: 1637`. + +### 3. Technical Lessons Learned (SO REST API) + +1. **Atomic PATCH (Stability):** Bundling all contact updates into a single `PATCH` request to the `/Contact/{id}` endpoint is far more stable than sequential UDF updates. If one field fails (e.g. invalid property), the whole transaction might roll back or partially fail—proactive validation is key. +2. **Website Sync (`Urls` Array):** Updating the website via REST requires manipulating the `Urls` array property. Simple field assignment to `UrlAddress` fails during `PATCH`. + * *Correct Format:* `"Urls": [{"Value": "https://example.com", "Description": "AI Discovered"}]`. +3. **List Resolution (`:DisplayText`):** To get the clean string value of a list field (like Campaign Name) without extra API calls, use the pseudo-field `ProgID:DisplayText` in the `$select` parameter. +4. **Field Length Limits:** Standard SuperOffice text UDFs are limited to approx. 140-254 characters. AI-generated summaries must be truncated (e.g. 132 chars) to avoid 400 Bad Request errors. +5. **Docker `env_file` Importance:** For production, mapping individual variables in `docker-compose.yml` is error-prone. Using `env_file: .env` ensures all services stay synchronized with the latest UDF IDs and mappings. +6. **Production URL Schema:** The production API is strictly hosted on `online3.superoffice.com` (for this tenant), while OAuth remains at `online.superoffice.com`. + +### 4. Campaign Trigger Logic +The `worker.py` (v1.8) now extracts the `campaign_tag` from `SuperOffice:23:DisplayText`. This tag is passed to the Company Explorer's provisioning API. If a matching entry exists in the `MarketingMatrix` for that tag, specific texts are used; otherwise, it falls back to the "standard" Kaltakquise texts. + +### 5. SuperOffice Authentication (Critical Update Feb 28, 2026) + +**Problem:** Authentication failures ("Invalid refresh token" or "Invalid client_id") occurred because standard `load_dotenv()` did not override stale environment variables present in the shell process. + +**Solution:** Always use `load_dotenv(override=True)` in Python scripts to force loading the actual values from the `.env` file. + +**Correct Authentication Pattern (Python):** +```python +from dotenv import load_dotenv +import os + +# CRITICAL: override=True ensures we read from .env even if env vars are already set +load_dotenv(override=True) + +client_id = os.getenv("SO_CLIENT_ID") +# ... +``` + +**Known Working Config (Production):** +* **Environment:** `online3` +* **Tenant:** `Cust26720` +* **Token Logic:** The `AuthHandler` implementation in `health_check_so.py` is the reference standard. Avoid using legacy `superoffice_client.py` without verifying it uses `override=True`. + +### 6. Sales & Opportunities (Roboplanet Specifics) + +When creating sales via API, specific constraints apply due to the shared tenant with Wackler: + +* **SaleTypeId:** MUST be **14** (`GE:"Roboplanet Verkauf";`) to ensure the sale is assigned to the correct business unit. + * *Alternative:* ID 16 (`GE:"Roboplanet Teststellung";`) for trials. +* **Mandatory Fields:** + * `Saledate` (Estimated Date): Must be provided in ISO format (e.g., `YYYY-MM-DDTHH:MM:SSZ`). + * `Person`: Highly recommended linking to a specific person, not just the company. +* **Context:** Avoid creating sales on the parent company "Wackler Service Group" (ID 3). Always target the specific lead company. + +### Analyse der SuperOffice `Sale`-Entität (März 2026) + +- **Ziel:** Erstellung eines Reports, der abbildet, welche Kunden welche Produkte angeboten bekommen oder gekauft haben. Die initiale Vermutung war, dass Produktinformationen oft als Freitext-Einträge und nicht über den offiziellen Produktkatalog erfasst werden. +- **Problem:** Die Untersuchung der Datenstruktur zeigte, dass die API-Endpunkte zur Abfrage von `Quote`-Objekten (Angeboten) und `QuoteLines` (Angebotspositionen) über `Sale`-, `Contact`- oder `Project`-Beziehungen hinweg nicht zuverlässig funktionierten. Viele Abfragen resultierten in `500 Internal Server Errors` oder leeren Datenmengen, was eine direkte Verknüpfung von Verkauf zu Produkt unmöglich machte. +- **Kern-Erkenntnis (Datenstruktur):** + 1. **Freitext statt strukturierter Daten:** Die Analyse eines konkreten `Sale`-Objekts (ID `342243`) bestätigte die ursprüngliche Hypothese. Produktinformationen (z.B. `2xOmnie CD-01 mit Nachlass`) werden direkt in das `Heading`-Feld (Betreff) des `Sale`-Objekts als Freitext eingetragen. Es existieren oft keine verknüpften `Quote`- oder `QuoteLine`-Entitäten. + 2. **Datenqualität bei Verknüpfungen:** Eine signifikante Anzahl von `Sale`-Objekten im System weist keine Verknüpfung zu einem `Contact`-Objekt auf (`Contact: null`). Dies erschwert die automatische Zuordnung von Verkäufen zu Kunden erheblich. +- **Nächster Schritt / Lösungsweg:** Ein Skript (`/app/connector-superoffice/generate_customer_product_report.py`) wurde entwickelt, das diese Probleme adressiert. Es fragt gezielt nur `Sale`-Objekte ab, die eine gültige `Contact`-Verknüpfung besitzen (`$filter=Contact ne null`). Anschließend extrahiert es den Kundennamen und das `Heading`-Feld des Verkaufs und durchsucht letzteres nach vordefinierten Produkt-Schlüsselwörtern. Die Ergebnisse werden für die manuelle Analyse in einer CSV-Datei (`product_report.csv`) gespeichert. Dieser Ansatz ist der einzig verlässliche Weg, um die gewünschten Informationen aus dem System zu extrahieren. + +### 7. Service & Tickets (Anfragen) + +SuperOffice Tickets represent the support and request system. Like Sales, they are organized to allow separation between Roboplanet and Wackler. + +* **Entity Name:** `ticket` +* **Roboplanet Specific Categories (CategoryId):** + * **ID 46:** `GE:"Lead Roboplanet";` + * **ID 47:** `GE:"Vertriebspartner Roboplanet";` + * **ID 48:** `GE:"Weitergabe Roboplanet";` + * **Hierarchical:** `Roboplanet/Support` (often used for technical issues). +* **Key Fields:** + * `ticketId`: Internal ID. + * `title`: The subject of the request. + * `contactId` / `personId`: Links to company and contact person. + * `ticketStatusId`: 1 (Unbearbeitet), 2 (In Arbeit), 3 (Bearbeitet). + * `ownedBy`: Often "ROBO" for Roboplanet staff. +* **Cross-Links:** Tickets can be linked to `saleId` (to track support during a sale) or `projectId`. + +--- +This is the core logic used to generate the company-specific opener. \ No newline at end of file diff --git a/Gruppenlisten_Output.txt b/Gruppenlisten_Output.txt deleted file mode 100644 index e1c3c091..00000000 --- a/Gruppenlisten_Output.txt +++ /dev/null @@ -1,385 +0,0 @@ -Kinderhaus St. Martin Neuching Kinderfotos Erding -02. - 05.06.2025 - -Nachname Vorname Gruppe ----------------------------------------------------------------- -Bauer Emilie Christiana Bienengruppe -Eberl Maximilian Bienengruppe -Eckinger Charly Bienengruppe -Ehrensberger Theo Bienengruppe -Gugic Vanessa Bienengruppe -Hermansdorfer Marina Bienengruppe -Kaiser Theresa Bienengruppe -Mehringer Quirin Bienengruppe -Müller Helene Bienengruppe -Nowak Juna Bienengruppe -Root Jonas Bienengruppe -Tress Anna Bienengruppe -Vass Marcell Bienengruppe -Vilgertshofer Clara Bienengruppe -Wamprechtshammer Marie Bienengruppe - -15 angemeldete Kinder - -Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden -Kinder an die Anmeldung erinnern. - -Stand 26.05.2025 20:04 Uhr - - -Kinderfotos Erding -Gartenstr. 10 85445 Oberding -www.kinderfotos-erding.de -08122-8470867 - ---- SEITENWECHSEL / PAGE BREAK --- - -Kinderhaus St. Martin Neuching Kinderfotos Erding -02. - 05.06.2025 - -Nachname Vorname Gruppe ---------------------------------------------------------------- -Arndt Alina Eulengruppe -Ben Hiba Elias Eulengruppe -Benke Bastian Eulengruppe -Berg Tamara Eulengruppe -Berndt Mariella Eulengruppe -Brose Valentina Eulengruppe -Drexler Lukas Eulengruppe -Eisenhofer Kilian Eulengruppe -Etterer Juna Eulengruppe -Fink Sophia Eulengruppe -Fleissner Sebastian Eulengruppe -Gebhardt Anton Eulengruppe -Götz Ludwig Eulengruppe -Josef Maximilian Eulengruppe -Klimaschewski Ben Eulengruppe -Kroh Louis Eulengruppe -Lavalie Alvin Eulengruppe -Michalik Fabian Eulengruppe -Multhammer Vincent Eulengruppe -Multhammer Vincent Ludwig Eulengruppe -Reck Sophia Eulengruppe -Richter Oskar Eulengruppe -Rocha Elias Lorenzo Eulengruppe -Rudolph Anna Eulengruppe -Ternavska Kira Eulengruppe -Varadi Amelie Eulengruppe -Varadi Clara Eulengruppe -Zerr Oskar Eulengruppe - -28 angemeldete Kinder - -Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden -Kinder an die Anmeldung erinnern. - -Stand 26.05.2025 20:04 Uhr - - -Kinderfotos Erding -Gartenstr. 10 85445 Oberding -www.kinderfotos-erding.de -08122-8470867 - ---- SEITENWECHSEL / PAGE BREAK --- - -Kinderhaus St. Martin Neuching Kinderfotos Erding -02. - 05.06.2025 - -Nachname Vorname Gruppe ------------------------------------------------------------------ -Bauer Luka Maximilian Željko Fröschegruppe -Birladeanu Ianis Andrei Fröschegruppe -Eberhardt Valentin Fröschegruppe -Gryczuk Oliver Fröschegruppe -Kelly Ferdinand Fröschegruppe -Koburger Leonie Fröschegruppe -Kressirer Josephine Fröschegruppe -Kunz Sophie Fröschegruppe -Käsmeier Xaver Fröschegruppe -Magin Lilly Fröschegruppe -Mert Lukas Fröschegruppe -Schmitz Juna Fröschegruppe -Schütz Mirjam Fröschegruppe -Wamprechtshammer Moritz Fröschegruppe -Wimmer Annika Fröschegruppe -Zerr Paul Fröschegruppe - -16 angemeldete Kinder - -Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden -Kinder an die Anmeldung erinnern. - -Stand 26.05.2025 20:04 Uhr - - -Kinderfotos Erding -Gartenstr. 10 85445 Oberding -www.kinderfotos-erding.de -08122-8470867 - ---- SEITENWECHSEL / PAGE BREAK --- - -Kinderhaus St. Martin Neuching Kinderfotos Erding -02. - 05.06.2025 - -Nachname Vorname Gruppe ---------------------------------------------------------------- -Albano Felicitas Hasengruppe -Ben Hiba Yassin Hasengruppe -Daiser Greta Hasengruppe -Fischer Alexander Hasengruppe -Gerlsbeck Lena Hasengruppe -Gottwalt Greta Hasengruppe -Gruber Sophia Hasengruppe -Hadzic Anelia Hasengruppe -Kundt Patrick Hasengruppe -Lechner Maximilian Hasengruppe -Numberger Benedikt Hasengruppe -Scherber Lukas Hasengruppe -Tress Leo Hasengruppe -Türe Emine Hasengruppe -Wagner Isabell Hasengruppe - -15 angemeldete Kinder - -Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden -Kinder an die Anmeldung erinnern. - -Stand 26.05.2025 20:04 Uhr - - -Kinderfotos Erding -Gartenstr. 10 85445 Oberding -www.kinderfotos-erding.de -08122-8470867 - ---- SEITENWECHSEL / PAGE BREAK --- - -Kinderhaus St. Martin Neuching Kinderfotos Erding -02. - 05.06.2025 - -Nachname Vorname Gruppe ------------------------------------------------------------------ -Brandl Mila Koboldegruppe -Engelking Melina Koboldegruppe -Fink Anna Koboldegruppe -Flügel Antonia Koboldegruppe -Heindl Louis Koboldegruppe -Kundt Philip Koboldegruppe -Lechner Ben Koboldegruppe -Müller Johanna Koboldegruppe -Schütz Rafaela Koboldegruppe -Seibold Marie Koboldegruppe -Seibold Sophia Koboldegruppe -Wenninger Rosa Koboldegruppe - -12 angemeldete Kinder - -Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden -Kinder an die Anmeldung erinnern. - -Stand 26.05.2025 20:04 Uhr - - -Kinderfotos Erding -Gartenstr. 10 85445 Oberding -www.kinderfotos-erding.de -08122-8470867 - ---- SEITENWECHSEL / PAGE BREAK --- - -Kinderhaus St. Martin Neuching Kinderfotos Erding -02. - 05.06.2025 - -Nachname Vorname Gruppe ---------------------------------------------------------------------- -Brose Marie Marienkäfergruppe -Burghardt Fanny Marienkäfergruppe -Daberger Tobias Marienkäfergruppe -Grobler Emelia Marienkäfergruppe -Hamdard Faria Marienkäfergruppe -Hermansdorfer Vincent Marienkäfergruppe -Huber Helena Marienkäfergruppe -Klaric Mia Marienkäfergruppe -Kraus Viktoria Marienkäfergruppe -Kroh Maximilian Marienkäfergruppe -Schuster Korbinian Marienkäfergruppe -Wenninger Valentin Marienkäfergruppe -Zehetmaier Emma Marienkäfergruppe - -13 angemeldete Kinder - -Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden -Kinder an die Anmeldung erinnern. - -Stand 26.05.2025 20:04 Uhr - - -Kinderfotos Erding -Gartenstr. 10 85445 Oberding -www.kinderfotos-erding.de -08122-8470867 - ---- SEITENWECHSEL / PAGE BREAK --- - -Kinderhaus St. Martin Neuching Kinderfotos Erding -02. - 05.06.2025 - -Nachname Vorname Gruppe -------------------------------------------------------------------- -Arndt Felix Maulwürfegruppe -Beck Josef Maulwürfegruppe -Ehrensberger Zeno Maulwürfegruppe -Eisenhofer Antonia Maulwürfegruppe -Farin Constantin Maulwürfegruppe -Fink Xaver Maulwürfegruppe -Fischer Joshua Maulwürfegruppe -Flügel Johann Maulwürfegruppe -Hadzic Aylin Maulwürfegruppe -Kressirer Simon Maulwürfegruppe -Kroh Liana Maulwürfegruppe -Kugler Milena Vaiana Maulwürfegruppe -Magin Lotte Maulwürfegruppe -Michalik Sarah Maulwürfegruppe -Rocha Elias Matteo Maulwürfegruppe -Schleier Valentin Maulwürfegruppe -Sedlmeir Julia Maulwürfegruppe -Suszczewicz Adam Maulwürfegruppe -Tratnik Maja Maulwürfegruppe -Wagner Mariella Maulwürfegruppe -Winkler Clara Maulwürfegruppe - -21 angemeldete Kinder - -Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden -Kinder an die Anmeldung erinnern. - -Stand 26.05.2025 20:04 Uhr - - -Kinderfotos Erding -Gartenstr. 10 85445 Oberding -www.kinderfotos-erding.de -08122-8470867 - ---- SEITENWECHSEL / PAGE BREAK --- - -Kinderhaus St. Martin Neuching Kinderfotos Erding -02. - 05.06.2025 - -Nachname Vorname Gruppe ------------------------------------------------------------------------- -Birladeanu Dima Matei Schmetterlingegruppe -Eichner Hanna Schmetterlingegruppe -Gruber Johanna Schmetterlingegruppe -Gschwendtner Theresa Schmetterlingegruppe -Haberthaler Moritz Schmetterlingegruppe -Hamdard Avesta Schmetterlingegruppe -Josef Konstantin Schmetterlingegruppe -Mikołajczak Gabriela Schmetterlingegruppe -Pfaus Leo Schmetterlingegruppe -Sulejmanovic Elna Schmetterlingegruppe - -10 angemeldete Kinder - -Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden -Kinder an die Anmeldung erinnern. - -Stand 26.05.2025 20:04 Uhr - - -Kinderfotos Erding -Gartenstr. 10 85445 Oberding -www.kinderfotos-erding.de -08122-8470867 - ---- SEITENWECHSEL / PAGE BREAK --- - -Kinderhaus St. Martin Neuching Kinderfotos Erding -02. - 05.06.2025 - -Nachname Vorname Gruppe ------------------------------------------------------------------ -Daiser Nora Wichtelgruppe -Fabri Jule Wichtelgruppe -Fink Leonie Wichtelgruppe -Gojanaj Simon Wichtelgruppe -Haberthaler Anton Wichtelgruppe -Hoffmann Eva Wichtelgruppe -Hoffmann Eva Charlotte Wichtelgruppe -Koburger Paula Wichtelgruppe -Mair Magdalena Wichtelgruppe -Sammer Emilie Wichtelgruppe - -10 angemeldete Kinder - -Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden -Kinder an die Anmeldung erinnern. - -Stand 26.05.2025 20:04 Uhr - - -Kinderfotos Erding -Gartenstr. 10 85445 Oberding -www.kinderfotos-erding.de -08122-8470867 - ---- SEITENWECHSEL / PAGE BREAK --- - -Kinderhaus St. Martin Neuching Kinderfotos Erding -02. - 05.06.2025 - -Nachname Vorname Gruppe ---------------------------------------------------------------- -Berndt Milena Wölfegruppe -Brose Sophia Wölfegruppe -Dressel Lucy Jolie Wölfegruppe -Etterer Nele Wölfegruppe -Fischer Benjamin Wölfegruppe -Götz Georg Wölfegruppe -Hermansdorfer Ludwig Wölfegruppe -Klimaschewski Leon Wölfegruppe -Magin Frederik Wölfegruppe -Müller Luisa Wölfegruppe -Niedermeier Lukas Wölfegruppe - -11 angemeldete Kinder - -Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden -Kinder an die Anmeldung erinnern. - -Stand 26.05.2025 20:04 Uhr - - -Kinderfotos Erding -Gartenstr. 10 85445 Oberding -www.kinderfotos-erding.de -08122-8470867 - ---- SEITENWECHSEL / PAGE BREAK --- - -Kinderhaus St. Martin Neuching Kinderfotos Erding -02. - 05.06.2025 - -Nachname Vorname Gruppe ------------------------------------------------------------------- -Baur Leon Malouis Zwergerlgruppe -Grobler Eloise Zwergerlgruppe -Hagn Maximilian Zwergerlgruppe -Hren Jan Zwergerlgruppe -Kugler Amaya Marina Zwergerlgruppe -Schmidt Emilie Zwergerlgruppe - -6 angemeldete Kinder - -Dies ist die Liste der bereits angemeldeten Kinder. Bitte die Eltern der noch fehlenden -Kinder an die Anmeldung erinnern. - -Stand 26.05.2025 20:04 Uhr - - -Kinderfotos Erding -Gartenstr. 10 85445 Oberding -www.kinderfotos-erding.de -08122-8470867 \ No newline at end of file diff --git a/HA_automations.yaml b/HA_automations.yaml deleted file mode 100644 index 474f9315..00000000 --- a/HA_automations.yaml +++ /dev/null @@ -1,1757 +0,0 @@ -- id: '1675939448073' - alias: Kühlschrank_Timer_An - description: '' - triggers: - - entity_id: sensor.kuehlschrank_power - above: 50 - trigger: numeric_state - conditions: [] - actions: - - data: - duration: 0 - target: - entity_id: timer.ks_an - action: timer.start - - action: timer.finish - metadata: {} - data: {} - target: - entity_id: timer.ks_aus - mode: single -- id: '1675939498815' - alias: Kühlschrank_Timer_aus - description: '' - triggers: - - entity_id: sensor.kuehlschrank_power - below: 50 - trigger: numeric_state - conditions: [] - actions: - - action: timer.finish - metadata: {} - data: {} - target: - entity_id: timer.ks_an - - data: - duration: 00:53:00 - target: - entity_id: timer.ks_aus - action: timer.start - mode: single -- id: '1677998809121' - alias: Akku muss laden Capacity - description: '' - triggers: - - entity_id: - - sensor.esphome_web_39b3f0_capacity_remaining_2 - below: 20 - trigger: numeric_state - conditions: [] - actions: - - type: turn_off - device_id: 20e71d487de794630ca8f98ed3ec5d44 - entity_id: switch.shelly_em3 - domain: switch - - type: turn_off - device_id: 07f49793e8ac90f480e75453fc2d6e4a - entity_id: switch.solaranlage - domain: switch - mode: single -- id: '1677998863398' - alias: Automatik an - description: '' - trigger: - - platform: numeric_state - entity_id: sensor.esphome_web_39b3f0_capacity_remaining_2 - above: 60 - condition: - - condition: numeric_state - entity_id: sensor.aktuelle_solarleistung - above: 300 - action: - - type: turn_on - device_id: 20e71d487de794630ca8f98ed3ec5d44 - entity_id: switch.shelly_em3 - domain: switch - - type: turn_on - device_id: 07f49793e8ac90f480e75453fc2d6e4a - entity_id: switch.solaranlage - domain: switch - mode: single -- id: '1678569907819' - alias: Akku_muss_laden_voltage - description: '' - trigger: - - platform: numeric_state - entity_id: sensor.esphome_web_39b3f0_average_cell_voltage_2 - below: 24 - for: - hours: 0 - minutes: 5 - seconds: 0 - condition: - - condition: and - conditions: - - condition: numeric_state - entity_id: sensor.esphome_web_39b3f0_average_cell_voltage - below: 24 - - condition: numeric_state - entity_id: sensor.hm600_spannung - below: 24 - enabled: false - - condition: or - conditions: - - condition: numeric_state - entity_id: sensor.esphome_web_39b3f0_average_cell_voltage_2 - below: 24 - - condition: numeric_state - entity_id: sensor.hm600_spannung - below: 24 - action: - - type: turn_off - device_id: 20e71d487de794630ca8f98ed3ec5d44 - entity_id: switch.shelly_em3 - domain: switch - - type: turn_off - device_id: 07f49793e8ac90f480e75453fc2d6e4a - entity_id: switch.solaranlage - domain: switch - mode: single -- id: '1679172454848' - alias: Toaster an - description: '' - triggers: - - entity_id: sensor.toaster_power - above: 350 - trigger: numeric_state - conditions: - - condition: or - conditions: - - condition: numeric_state - entity_id: sensor.esphome_web_39b3f0_capacity_remaining - above: 30 - - condition: device - type: is_on - device_id: 20e71d487de794630ca8f98ed3ec5d44 - entity_id: switch.shelly_em3 - domain: switch - actions: - - type: turn_on - device_id: 655ddbb8d1d5372f27092571cea2f1ee - entity_id: switch.akku - domain: switch - - data: - qos: 0 - retain: false - topic: inverter/ctrl/limit/0 - payload_template: 350W - action: mqtt.publish - - type: turn_off - device_id: b1390014b64d249716aa583480a364cf - entity_id: 3639bc3fea7e4b6c5bda747cf0d80d65 - domain: switch - mode: single -- id: '1679172510208' - alias: Toaster aus - description: '' - triggers: - - entity_id: sensor.toaster_power - below: 349.9 - trigger: numeric_state - conditions: - - condition: device - type: is_on - device_id: 20e71d487de794630ca8f98ed3ec5d44 - entity_id: 19fe306d346018adab762735b3c6489f - domain: switch - actions: - - type: turn_off - device_id: 655ddbb8d1d5372f27092571cea2f1ee - entity_id: switch.akku - domain: switch - - data: - qos: 0 - retain: false - topic: inverter/ctrl/limit/0 - payload_template: 85W - action: mqtt.publish - - type: turn_on - device_id: b1390014b64d249716aa583480a364cf - entity_id: 3639bc3fea7e4b6c5bda747cf0d80d65 - domain: switch - mode: single -- id: '1680504779348' - alias: Solaranlage-Anschalten > 30% - description: '' - trigger: - - platform: numeric_state - entity_id: - - sensor.esphome_web_39b3f0_capacity_remaining_2 - above: 30 - condition: - - condition: numeric_state - entity_id: sensor.solaranlage_power - above: 99.9 - enabled: true - action: - - type: turn_on - device_id: 07f49793e8ac90f480e75453fc2d6e4a - entity_id: switch.solaranlage - domain: switch - - data: - qos: 0 - retain: false - topic: inverter/ctrl/limit_nonpersistent_absolute/ - payload: '85' - action: mqtt.publish - mode: single -- id: manual_trigger_automation - alias: Manual Trigger Automation - trigger: - - platform: state - entity_id: input_boolean.manual_trigger - to: 'on' - action: - - service: mqtt.publish - data: - qos: 0 - retain: false - topic: inverter/ctrl/limit_nonpersistent_absolute/ - payload: '300' - - type: turn_on - device_id: 655ddbb8d1d5372f27092571cea2f1ee - entity_id: switch.akku - domain: switch - - delay: - hours: 0 - minutes: 10 - seconds: 0 - milliseconds: 0 - - type: turn_off - device_id: 655ddbb8d1d5372f27092571cea2f1ee - entity_id: switch.akku - domain: switch - - service: mqtt.publish - data: - qos: 0 - retain: false - topic: inverter/ctrl/limit_nonpersistent_absolute/ - payload: '85' - - service: input_boolean.turn_off - entity_id: input_boolean.manual_trigger - mode: single -- id: '1681113967238' - alias: Akku_muss_laden hm600 Voltage - description: '' - triggers: - - entity_id: sensor.hm600_spannung - for: - hours: 0 - minutes: 15 - seconds: 0 - below: 25 - trigger: numeric_state - conditions: [] - actions: - - type: turn_off - device_id: 655ddbb8d1d5372f27092571cea2f1ee - entity_id: switch.akku - domain: switch - - type: turn_off - device_id: 07f49793e8ac90f480e75453fc2d6e4a - entity_id: switch.solaranlage - domain: switch - - data: - qos: 0 - retain: false - topic: inverter/ctrl/limit_nonpersistent_absolute/ - payload: '85' - action: mqtt.publish - - device_id: ac306576e7e4a19654c6c229794b978c - domain: mobile_app - type: notify - message: Solaranlage aufgrund Unterspannung abgeschaltet - mode: single -- id: '1682766252982' - alias: High_Load_Start - description: '' - trigger: - - platform: numeric_state - entity_id: sensor.total_power - for: - hours: 0 - minutes: 0 - seconds: 10 - above: 400 - condition: - - condition: device - type: is_on - device_id: 20e71d487de794630ca8f98ed3ec5d44 - entity_id: switch.shelly_em3 - domain: switch - action: - - type: turn_on - device_id: 655ddbb8d1d5372f27092571cea2f1ee - entity_id: switch.akku - domain: switch - - service: mqtt.publish - data: - qos: 0 - retain: false - topic: inverter/ctrl/limit/0 - payload: 400W - enabled: false - mode: single -- id: '1682766378341' - alias: Einspeisung Verhindern - description: '' - trigger: - - platform: numeric_state - entity_id: sensor.total_power - for: - hours: 0 - minutes: 0 - seconds: 40 - below: -30 - condition: - - type: is_value - condition: device - device_id: cd0349c647cf916da50cbd4574ca7c04 - entity_id: 27febbda0691f34ee92742206a717596 - domain: sensor - below: 99 - action: - - type: turn_off - device_id: 655ddbb8d1d5372f27092571cea2f1ee - entity_id: switch.akku - domain: switch - enabled: false - - service: mqtt.publish - data: - qos: 0 - retain: false - topic: inverter/ctrl/limit/0 - payload: 85W - mode: single -- id: '1682774344623' - alias: Solaranpassung_manuell - description: '' - trigger: - - platform: state - entity_id: - - input_number.solarausgabe - condition: - - condition: device - type: is_on - device_id: 20e71d487de794630ca8f98ed3ec5d44 - entity_id: switch.shelly_em3 - domain: switch - enabled: false - action: - - service: mqtt.publish - data: - topic: inverter/ctrl/limit/0 - qos: 0 - retain: false - payload: '{{ states(''input_number.solarausgabe'')|float }}W - - ' - mode: single -- id: '1683617261263' - alias: High_Load_Stop - description: '' - trigger: - - platform: numeric_state - entity_id: sensor.total_power - for: - hours: 0 - minutes: 0 - seconds: 15 - below: -20.3 - condition: - - condition: device - type: is_on - device_id: 655ddbb8d1d5372f27092571cea2f1ee - entity_id: switch.akku - domain: switch - action: - - type: turn_off - device_id: 655ddbb8d1d5372f27092571cea2f1ee - entity_id: switch.akku - domain: switch - - service: mqtt.publish - data: - qos: 0 - retain: false - topic: inverter/ctrl/limit/0 - payload: 85W - enabled: false - mode: single -- id: '1695804992987' - alias: Uhr_Textnachricht - description: '' - use_blueprint: - path: homeassistant/smarthomejunkie/awtrix_create_notification.yaml - input: - awtrix_displays: - - 8d1c339ffbe0e8762e94c8b1ef359207 - toggle_helper: input_boolean.nachricht - notification_text: SpaceX Starship IFT-2 - TODAY - excitement guaranteed - my_icon: '3709' - show_rainbow: false - scrollspeed: 63 - hold_notification: false - repeat: '1' -- id: '1695805241325' - alias: Uhr_Stromverbrauch-Anzeigen - description: '' - use_blueprint: - path: homeassistant/smarthomejunkie/awtrix_create_sensor_app.yaml - input: - awtrix_displays: - - 8d1c339ffbe0e8762e94c8b1ef359207 - toggle_helper: input_boolean.stromverbrauch - my_sensor: sensor.durchschnittsverbrauch - my_icon: '54231' - duration: '4' -- id: '1695813279896' - alias: Uhr_Animation - description: '' - use_blueprint: - path: homeassistant/smarthomejunkie/awtrix_create_notification.yaml - input: - awtrix_displays: - - 8d1c339ffbe0e8762e94c8b1ef359207 - toggle_helper: input_boolean.uhr_animation - my_icon: eyes - repeat: '3' - hold_notification: true -- id: '1695815804102' - alias: Uhr_Eigenwerbung - description: '' - use_blueprint: - path: homeassistant/smarthomejunkie/awtrix_create_notification.yaml - input: - awtrix_displays: - - 8d1c339ffbe0e8762e94c8b1ef359207 - toggle_helper: input_boolean.uhr_eigenwerbung - notification_text: Hallo Raisa, schön, dass Du da bist! - text_color: - - 255 - - 0 - - 0 - hold_notification: false - repeat: '2' - duration: '10' - show_rainbow: true -- id: '1695817032461' - alias: Uhr_aussentemperatur - description: '' - use_blueprint: - path: homeassistant/smarthomejunkie/awtrix_create_sensor_app.yaml - input: - awtrix_displays: - - 8d1c339ffbe0e8762e94c8b1ef359207 - toggle_helper: input_boolean.uhr_temperatur_draussen - my_sensor: sensor.schreibtisch_temperature - my_icon: '4285' - duration: '4' -- id: '1695820890983' - alias: Uhr_Essen - description: '' - use_blueprint: - path: homeassistant/smarthomejunkie/awtrix_create_notification.yaml - input: - awtrix_displays: - - 8d1c339ffbe0e8762e94c8b1ef359207 - toggle_helper: input_boolean.uhr_essen - notification_text: 'Heutiges Abendessen: Leckere Wraps mit diversen Füllungen' - my_icon: '47381' -- id: '1695829709063' - alias: Uhr_Kalender - description: '' - use_blueprint: - path: homeassistant/smarthomejunkie/awtrix_list_calendar.yaml - input: - awtrix_displays: - - 8d1c339ffbe0e8762e94c8b1ef359207 - toggle_helper: input_boolean.uhr_kalender - my_calendar: calendar.kinderfotos_erding -- id: '1695841797705' - alias: Uhr_animation2 - description: '' - use_blueprint: - path: homeassistant/smarthomejunkie/awtrix_create_notification.yaml - input: - awtrix_displays: - - 8d1c339ffbe0e8762e94c8b1ef359207 - toggle_helper: input_boolean.uhr_animation_snake - effect: Snake -- id: '1695845338845' - alias: Uhr nachts ausschalten Wochentags - description: '' - trigger: - - platform: time - at: '22:00:00' - condition: - - condition: time - weekday: - - mon - - tue - - wed - - thu - - sun - action: - - type: turn_off - device_id: 8d1c339ffbe0e8762e94c8b1ef359207 - entity_id: 3777e42274833e2e01f1f058db2c1cb2 - domain: light - mode: single -- id: '1695845520168' - alias: 'Uhr morgens einschalten ' - description: '' - trigger: - - platform: time - at: 06:00:00 - condition: [] - action: - - type: turn_on - device_id: 8d1c339ffbe0e8762e94c8b1ef359207 - entity_id: 3777e42274833e2e01f1f058db2c1cb2 - domain: light - mode: single -- id: '1695882314624' - alias: Uhr_App Timer - description: '' - use_blueprint: - path: homeassistant/smarthomejunkie/awtrix_set_transition_time.yaml - input: - awtrix_displays: - - 8d1c339ffbe0e8762e94c8b1ef359207 - apptime_helper: input_number.uhr_app_timer -- id: '1696359376941' - alias: Flur_bewegungslicht_yama - description: '' - use_blueprint: - path: networkingcat/yet_another_motion_automation.yaml - input: - motion_entity: device_tracker.42lw579s_zd - no_motion_wait: 60 - light_target: - entity_id: light.shellyplug_s_e43681 - automation_blocker: binary_sensor.helligkeit_solar - automation_blocker_boolean: true -- id: '1696483827070' - alias: Flutlicht an Lampe aus - description: '' - trigger: - - platform: device - type: turned_on - device_id: 7ca1eb42f11ebfc1be1a15604627f8a7 - entity_id: e074c80c61793f145a747e33295d8d9b - domain: light - condition: [] - action: - - type: turn_off - device_id: 28b00b236f318fdea8f9f1e12327aaec - entity_id: c5ce887fdf8d90966c68ff1c8faf888c - domain: light - mode: single -- id: '1696603504158' - alias: FLUR BLEIBT AN BEI bewegung - description: '' - trigger: - - platform: device - type: turned_on - device_id: 7ca1eb42f11ebfc1be1a15604627f8a7 - entity_id: e074c80c61793f145a747e33295d8d9b - domain: light - condition: - - type: is_motion - condition: device - device_id: d421ddc5d65d47e31e5b83d68ed63e7d - entity_id: 46e347691937743d823fb3fd5776cc6b - domain: binary_sensor - action: - - type: turn_on - device_id: 7ca1eb42f11ebfc1be1a15604627f8a7 - entity_id: e074c80c61793f145a747e33295d8d9b - domain: light - mode: single -- id: '1696879909020' - alias: Flurlicht_Automatisierung_eigen - description: '' - trigger: - - platform: state - entity_id: - - binary_sensor.bewegungsmelder_motion - to: 'on' - id: motion_on - - platform: state - entity_id: - - binary_sensor.bewegungsmelder_motion - to: 'off' - for: - hours: 0 - minutes: 2 - seconds: 0 - id: motion_off - condition: - - condition: state - entity_id: binary_sensor.helligkeit_solar - state: 'on' - action: - - choose: - - conditions: - - condition: trigger - id: - - motion_on - - condition: numeric_state - entity_id: sensor.aktuelle_solarleistung - above: 50 - sequence: - - service: light.turn_on - data: {} - target: - entity_id: - - light.shellyplug_s_e43681 - - light.flurlicht - - conditions: - - condition: trigger - id: - - motion_off - sequence: - - service: light.turn_off - data: {} - target: - entity_id: - - light.shellyplug_s_e43681 - - light.flurlicht - mode: single -- id: '1696880501607' - alias: Flurlicht_automatisierung_eigen - description: '' - trigger: - - platform: device - type: turned_on - device_id: 7ca1eb42f11ebfc1be1a15604627f8a7 - entity_id: e074c80c61793f145a747e33295d8d9b - domain: light - - platform: state - entity_id: - - binary_sensor.bewegungsmelder_motion - from: 'on' - id: motion_on - - platform: state - entity_id: - - binary_sensor.bewegungsmelder_motion - to: 'off' - for: - hours: 0 - minutes: 3 - seconds: 0 - id: motoin_off - condition: [] - action: - - choose: - - conditions: - - condition: trigger - id: - - motion_on - sequence: - - service: light.turn_on - data: {} - target: - entity_id: - - light.shellyplug_s_e43681 - - switch.flurlampe - - delay: - hours: 0 - minutes: 2 - seconds: 0 - milliseconds: 0 - - conditions: - - condition: trigger - id: - - motoin_off - sequence: - - service: light.turn_off - data: {} - target: - entity_id: - - light.shellyplug_s_e43681 - - switch.flurlampe - mode: single -- id: '1698566177352' - alias: Bt Kopfhörer verbunden - description: '' - trigger: - - platform: state - entity_id: - - sensor.sm_x200_bluetooth_connection - to: B4:4D:43:8F:2E:BE (SOUNDPEATS RunFree) - condition: [] - action: - - type: turn_on - device_id: cc748d3b4fffe325784709615582776c - entity_id: 5c6d5b62c8a78f849f417015ecfb361c - domain: light - - delay: - hours: 0 - minutes: 0 - seconds: 3 - milliseconds: 0 - - type: turn_off - device_id: cc748d3b4fffe325784709615582776c - entity_id: 5c6d5b62c8a78f849f417015ecfb361c - domain: light - mode: single -- id: '1698571355574' - alias: Template_Test_Handy - description: '' - trigger: - - platform: template - value_template: '{{ ''13:17:01:08:26:2B (POWERADD S12 )'' in state_attr(''sensor.SM-528B_bluetooth_connection'', - ''connected_paired_devices'') }}' - condition: [] - action: - - type: turn_on - device_id: 70266940a499534237a452f02d812ca7 - entity_id: bb8bbf788cabe0795deca360dcd8a46b - domain: light - - delay: - hours: 0 - minutes: 0 - seconds: 3 - milliseconds: 0 - - type: turn_off - device_id: 70266940a499534237a452f02d812ca7 - entity_id: bb8bbf788cabe0795deca360dcd8a46b - domain: light - mode: single -- id: '1698587408977' - alias: 'Winter: Sport_Start' - description: '' - trigger: - - platform: state - entity_id: - - sensor.sm_x200_bluetooth_connection - condition: - - condition: state - entity_id: sensor.sm_a528b_bluetooth_connection - attribute: connected_paired_devices - enabled: false - state: '' - - condition: time - after: 05:55:00 - before: 06:05:00 - weekday: - - mon - - tue - - wed - - thu - - fri - enabled: true - action: - - type: turn_on - device_id: 70266940a499534237a452f02d812ca7 - entity_id: bb8bbf788cabe0795deca360dcd8a46b - domain: light - - delay: - hours: 0 - minutes: 1 - seconds: 0 - milliseconds: 0 - - type: turn_off - device_id: 70266940a499534237a452f02d812ca7 - entity_id: bb8bbf788cabe0795deca360dcd8a46b - domain: light - mode: single -- id: '1698598421938' - alias: 'Winter: Sport fertig' - description: '' - trigger: - - platform: state - entity_id: - - sensor.sm_x200_bluetooth_connection - condition: - - condition: state - entity_id: sensor.sm_a528b_bluetooth_connection - attribute: connected_paired_devices - enabled: false - state: '' - - condition: time - after: 06:25:00 - before: 06:40:00 - weekday: - - mon - - tue - - wed - - thu - - fri - enabled: true - action: - - type: turn_on - device_id: 70266940a499534237a452f02d812ca7 - entity_id: bb8bbf788cabe0795deca360dcd8a46b - domain: light - - delay: - hours: 0 - minutes: 1 - seconds: 0 - milliseconds: 0 - - type: turn_off - device_id: 70266940a499534237a452f02d812ca7 - entity_id: bb8bbf788cabe0795deca360dcd8a46b - domain: light - mode: single -- id: '1698607877409' - alias: Aufstehen_Christian - description: '' - trigger: - - platform: device - type: changed_states - device_id: 644c04750e25378d9006c21260170e29 - entity_id: b3c6f6ee9de62c23052fa0da56109b1a - domain: light - condition: - - condition: time - after: 05:50:00 - before: 05:57:00 - - condition: device - type: is_on - device_id: 87ccc9b1dbf3f05aee621ad8ad2cc6b3 - entity_id: 950ebe281f9dd2a2be1ac45d7af0a8d2 - domain: light - action: - - type: turn_off - device_id: 87ccc9b1dbf3f05aee621ad8ad2cc6b3 - entity_id: 950ebe281f9dd2a2be1ac45d7af0a8d2 - domain: light - - type: turn_off - device_id: 644c04750e25378d9006c21260170e29 - entity_id: b3c6f6ee9de62c23052fa0da56109b1a - domain: light - mode: single -- id: '1698608840845' - alias: Tobis Wecker - description: '' - trigger: - - platform: time - at: 05:20:00 - condition: - - condition: and - conditions: - - condition: state - entity_id: calendar.bavaria_holidays - state: 'off' - - condition: time - weekday: - - mon - - tue - - wed - - condition: state - entity_id: input_boolean.verreist - state: 'off' - action: - - type: turn_on - device_id: 87ccc9b1dbf3f05aee621ad8ad2cc6b3 - entity_id: 950ebe281f9dd2a2be1ac45d7af0a8d2 - domain: light - mode: single -- id: '1698608920624' - alias: Tobis_Morgenroutine - description: '' - trigger: - - platform: device - type: turned_on - device_id: 382e14995c1ff9a4d9deeb0f03f86993 - entity_id: b885303c6ad7549daa287909cab1031c - domain: light - condition: - - condition: time - after: 05:20:00 - before: 05:45:00 - weekday: - - mon - - tue - - wed - - thu - - fri - action: - - type: turn_off - device_id: 87ccc9b1dbf3f05aee621ad8ad2cc6b3 - entity_id: 950ebe281f9dd2a2be1ac45d7af0a8d2 - domain: light - mode: single -- id: '1698609162926' - alias: Christians Wecker - description: '' - trigger: - - platform: time - at: 05:52:00 - enabled: false - condition: - - condition: and - conditions: - - condition: state - entity_id: calendar.bavaria_holidays - state: 'off' - - condition: time - weekday: - - mon - - tue - - wed - - thu - - fri - - condition: state - entity_id: input_boolean.verreist - state: 'off' - - condition: device - device_id: ac306576e7e4a19654c6c229794b978c - domain: device_tracker - entity_id: 128fad5cb58568647b5c71ebdc798e41 - type: is_home - enabled: false - action: - - type: turn_on - device_id: 87ccc9b1dbf3f05aee621ad8ad2cc6b3 - entity_id: 950ebe281f9dd2a2be1ac45d7af0a8d2 - domain: light - mode: single -- id: '1698609272546' - alias: Christians_Morgenroutine - description: '' - trigger: - - platform: device - type: changed_states - device_id: 644c04750e25378d9006c21260170e29 - entity_id: b3c6f6ee9de62c23052fa0da56109b1a - domain: light - condition: - - condition: time - after: 05:52:00 - before: 05:55:00 - action: - - type: turn_off - device_id: 644c04750e25378d9006c21260170e29 - entity_id: b3c6f6ee9de62c23052fa0da56109b1a - domain: light - - delay: - hours: 0 - minutes: 0 - seconds: 30 - milliseconds: 0 - - type: turn_off - device_id: 87ccc9b1dbf3f05aee621ad8ad2cc6b3 - entity_id: 950ebe281f9dd2a2be1ac45d7af0a8d2 - domain: light - mode: single -- id: '1699304472370' - alias: Neuew flurlicht - description: '' - use_blueprint: - path: networkingcat/yet_another_motion_automation.yaml - input: - motion_entity: binary_sensor.bewegungsmelder_motion - light_target: - entity_id: light.shellyplug_s_e43681 -- id: '1699617083047' - alias: AWTRIX - Team Scoreboard - description: '' - use_blueprint: - path: fettesb/awtrix_nfl_team_scoreboard.yaml - input: - awtrix: - - 8d1c339ffbe0e8762e94c8b1ef359207 - my_sensor: sensor.bayern2 - appname: teamscoreby -- id: '1699822172717' - alias: Benachrichtigung_Tablet-niedriger Akku - description: '' - trigger: - - type: battery_level - device_id: c73798900d32d39cb9b309821521c5bc - entity_id: a2414ebb1c8e407c0ffa0f0cf0e5dd5d - domain: sensor - below: 25 - condition: - - condition: time - weekday: - - mon - - tue - - wed - - thu - - sun - after: '21:00:00' - action: - - service: notify.mobile_app_sm_a528b - data: - message: Tablet aufladen, Akkustand ist niedrig - mode: single -- id: '1703011893140' - alias: Wohnzimmerfenster zu lange offen - description: '' - trigger: - - type: opened - platform: device - device_id: 939fa49367827df4a883d2d3a66aa010 - entity_id: b4685542a327045cb4f20ae04b494acd - domain: binary_sensor - for: - hours: 0 - minutes: 10 - seconds: 0 - condition: - - condition: numeric_state - entity_id: sensor.shellyplusht_08b61fce419c_temperature - above: sensor.garten_temperature - action: - - service: notify.mobile_app_sm_a528b - data: - message: Fenster Wohnzimmer ist zu lange offen - title: Fenter schließen - mode: single -- id: '1703577375393' - alias: Schlafzimmerfenster zu lange offen - description: '' - trigger: - - type: opened - platform: device - device_id: 9f8c74df5e05fd6fb4718521f951a760 - entity_id: 1e98846b8b4a4146743e64f6e1c5fc00 - domain: binary_sensor - for: - hours: 0 - minutes: 10 - seconds: 0 - condition: - - condition: numeric_state - entity_id: sensor.shellyplusht_08b61fce419c_temperature - above: sensor.garten_temperature - action: - - service: notify.mobile_app_sm_a528b - data: - message: Fenster Schlafzimmer ist zu lange offen - title: Fenter schließen - - device_id: 3abfd883b327cb2ce142326c0a3bc28b - domain: climate - entity_id: c9bfc7b59e5413bab16257523e9171c9 - type: set_hvac_mode - hvac_mode: 'off' - mode: single -- id: '1703577438040' - alias: Küchenfenster zu lange offen - description: '' - trigger: - - type: opened - platform: device - device_id: 7da0e5eddcc7e03783b8aec334cbea84 - entity_id: d643330bf7dfbfd913d46a88c3adc676 - domain: binary_sensor - for: - hours: 0 - minutes: 15 - seconds: 0 - condition: - - condition: numeric_state - entity_id: sensor.shellyplusht_08b61fce419c_temperature - above: sensor.garten_temperature - action: - - service: notify.mobile_app_sm_a528b - data: - message: Fenster Küche ist zu lange offen - title: Fenter schließen - mode: single -- id: '1703879103971' - alias: Uhr ausschalten Wochenende - description: '' - trigger: - - platform: time - at: '23:00:00' - condition: - - condition: time - weekday: - - fri - - sat - action: - - type: turn_off - device_id: 8d1c339ffbe0e8762e94c8b1ef359207 - entity_id: 3777e42274833e2e01f1f058db2c1cb2 - domain: light - mode: single -- id: '1704560933225' - alias: solaranzeige_awtrix - description: '' - trigger: - - platform: device - type: turned_on - device_id: 07f49793e8ac90f480e75453fc2d6e4a - entity_id: 53440250e122983c8bf30d865bce9b0d - domain: switch - alias: solar-an - - platform: device - type: turned_off - device_id: 07f49793e8ac90f480e75453fc2d6e4a - entity_id: 53440250e122983c8bf30d865bce9b0d - domain: switch - alias: solar-aus - condition: [] - action: - - choose: - - conditions: - - condition: trigger - id: solar-an - sequence: - - type: turn_on - device_id: 8d1c339ffbe0e8762e94c8b1ef359207 - entity_id: 0d6b10ec3f4a5d1f0c8702f6d0964eb5 - domain: light - - conditions: - - condition: trigger - id: solar-aus - sequence: - - type: turn_off - device_id: 8d1c339ffbe0e8762e94c8b1ef359207 - entity_id: 0d6b10ec3f4a5d1f0c8702f6d0964eb5 - domain: light - mode: single -- id: '1705262664413' - alias: Müll-Benachrichtigung-Gelber Sack - description: '' - trigger: - - platform: time - at: '18:00:00' - condition: - - condition: or - conditions: - - condition: template - value_template: '{{''Morgen'' in states(''sensor.gelber_sack'') }}' - - condition: template - value_template: '{{''in 2 Tagen'' in states(''sensor.gelber_sack'') }}' - action: - - service: notify.mobile_app_sm_a528b - data: - message: Abholung {{states('sensor.gelber_sack') }} - mode: single -- id: '1705262750163' - alias: Müll-Benachrichtigung-Restmüll - description: '' - trigger: - - platform: time - at: '18:00:00' - condition: - - condition: or - conditions: - - condition: template - value_template: '{{''Morgen'' in states(''sensor.restmull'') }}' - - condition: template - value_template: '{{''in 2 Tagen'' in states(''sensor.restmull'') }}' - action: - - service: notify.mobile_app_sm_a528b - data: - message: Abholung {{states('sensor.restmull') }} - mode: single -- id: '1705262850442' - alias: Müll-Benachrichtigung-Biotonne - description: '' - trigger: - - platform: time - at: '18:00:00' - condition: - - condition: or - conditions: - - condition: template - value_template: '{{''Morgen'' in states(''sensor.biotonne'') }}' - - condition: template - value_template: '{{''in 2 Tagen'' in states(''sensor.biotonne'') }}' - action: - - service: notify.mobile_app_sm_a528b - data: - message: Abholung {{states('sensor.biotonne') }} - mode: single -- id: '1706793617634' - alias: Uhr_Solar_An - description: '' - trigger: - - platform: device - type: turned_on - device_id: 07f49793e8ac90f480e75453fc2d6e4a - entity_id: 53440250e122983c8bf30d865bce9b0d - domain: switch - condition: [] - action: - - type: turn_on - device_id: 8d1c339ffbe0e8762e94c8b1ef359207 - entity_id: 0d6b10ec3f4a5d1f0c8702f6d0964eb5 - domain: light - mode: single -- id: '1706793646756' - alias: Uhr_Solar-Aus - description: '' - trigger: - - platform: device - type: turned_off - device_id: 07f49793e8ac90f480e75453fc2d6e4a - entity_id: 53440250e122983c8bf30d865bce9b0d - domain: switch - condition: [] - action: - - type: turn_off - device_id: 8d1c339ffbe0e8762e94c8b1ef359207 - entity_id: 0d6b10ec3f4a5d1f0c8702f6d0964eb5 - domain: light - mode: single -- id: '1709980832696' - alias: Uhr_Solar_Automatik_An - description: '' - trigger: - - platform: device - type: turned_on - device_id: 20e71d487de794630ca8f98ed3ec5d44 - entity_id: 19fe306d346018adab762735b3c6489f - domain: switch - condition: [] - action: - - type: turn_on - device_id: 8d1c339ffbe0e8762e94c8b1ef359207 - entity_id: f021c00a7c37f211904478f28e5e5b23 - domain: light - mode: single -- id: '1709980943349' - alias: Uhr_Solar_Automatik_Aus - description: '' - trigger: - - platform: device - type: turned_off - device_id: 20e71d487de794630ca8f98ed3ec5d44 - entity_id: 19fe306d346018adab762735b3c6489f - domain: switch - condition: [] - action: - - type: turn_off - device_id: 8d1c339ffbe0e8762e94c8b1ef359207 - entity_id: f021c00a7c37f211904478f28e5e5b23 - domain: light - mode: single -- id: '1714850348105' - alias: Wechselschaltung - description: '' - trigger: - - platform: state - entity_id: - - input_boolean.neg_30_w - from: 'on' - to: 'off' - id: -30w-on_to_off - - platform: state - entity_id: - - input_boolean.neg_30_w - id: -30w_off_to_on - from: 'off' - to: 'on' - condition: [] - action: - - if: - - condition: trigger - id: - - -30w-on_to_off - then: - - service: input_boolean.turn_on - metadata: {} - data: {} - target: - entity_id: input_boolean.plus_30_w - else: - - if: - - condition: trigger - id: - - -30w_off_to_on - then: - - service: input_boolean.turn_off - metadata: {} - data: {} - target: - entity_id: input_boolean.plus_30_w - mode: single -- id: '1718693017099' - alias: Hoher_Strombezug Kühlschrank aus - description: '' - trigger: - - platform: numeric_state - entity_id: - - sensor.total_power_v2 - for: - hours: 0 - minutes: 0 - seconds: 30 - above: 200 - condition: - - type: is_power - condition: device - device_id: e1fdbb35702a6689dd4abf0cd1181e68 - entity_id: 6a4e9a43aa830c65ebf11e3d8ea38411 - domain: sensor - above: 60 - action: - - type: turn_off - device_id: e1fdbb35702a6689dd4abf0cd1181e68 - entity_id: 880ed9b78f19ae1fcdf6c476b14bc607 - domain: switch - mode: single -- id: '1718693085150' - alias: Hoher-Strombezug-vorbei Kühlschrank an - description: '' - triggers: - - entity_id: - - sensor.total_power_v2 - for: - hours: 0 - minutes: 4 - seconds: 0 - below: 200 - trigger: numeric_state - conditions: [] - actions: - - type: turn_on - device_id: e1fdbb35702a6689dd4abf0cd1181e68 - entity_id: 880ed9b78f19ae1fcdf6c476b14bc607 - domain: switch - - type: turn_on - device_id: b1390014b64d249716aa583480a364cf - entity_id: 3639bc3fea7e4b6c5bda747cf0d80d65 - domain: switch - mode: single -- id: '1718792759056' - alias: Hoher Strombezug Laptop aus - description: '' - triggers: - - entity_id: - - sensor.total_power_v2 - for: - hours: 0 - minutes: 0 - seconds: 10 - above: 300 - trigger: numeric_state - - type: turned_on - device_id: 07f49793e8ac90f480e75453fc2d6e4a - entity_id: 53440250e122983c8bf30d865bce9b0d - domain: switch - trigger: device - - entity_id: - - sensor.aktuelle_solarleistung_2 - above: 300 - trigger: numeric_state - conditions: [] - actions: - - type: turn_off - device_id: b1390014b64d249716aa583480a364cf - entity_id: 3639bc3fea7e4b6c5bda747cf0d80d65 - domain: switch - - type: turn_off - device_id: e1fdbb35702a6689dd4abf0cd1181e68 - entity_id: 880ed9b78f19ae1fcdf6c476b14bc607 - domain: switch - mode: single -- id: '1725286699918' - alias: Volleinspeisung abschalten - description: Schaltet den Auotmatikmodus wieder ein wenn über 100 W aus dem Akku - entzogen werden. - trigger: - - platform: numeric_state - entity_id: - - sensor.esphome_web_39b3f0_discharging_power_2 - for: - hours: 0 - minutes: 1 - seconds: 0 - above: 100 - - platform: numeric_state - entity_id: - - sensor.esphome_web_39b3f0_capacity_remaining_2 - below: 97 - condition: - - condition: numeric_state - entity_id: sensor.limit - above: 40 - - condition: state - entity_id: switch.shelly_em3 - state: 'off' - action: - - type: turn_on - device_id: 20e71d487de794630ca8f98ed3ec5d44 - entity_id: 19fe306d346018adab762735b3c6489f - domain: switch - mode: single -- id: '1726687388726' - alias: 'Fehler Solaranlage Neustart ' - description: '' - trigger: - - platform: device - type: turned_on - device_id: 07f49793e8ac90f480e75453fc2d6e4a - entity_id: 53440250e122983c8bf30d865bce9b0d - domain: switch - condition: - - type: is_power - condition: device - device_id: 07f49793e8ac90f480e75453fc2d6e4a - entity_id: 8d27a237619bd65fe8f869c376789227 - domain: sensor - below: 10 - action: - - type: turn_off - device_id: 07f49793e8ac90f480e75453fc2d6e4a - entity_id: 53440250e122983c8bf30d865bce9b0d - domain: switch - - delay: - hours: 0 - minutes: 0 - seconds: 20 - milliseconds: 0 - - type: turn_on - device_id: 07f49793e8ac90f480e75453fc2d6e4a - entity_id: 53440250e122983c8bf30d865bce9b0d - domain: switch - - delay: - hours: 0 - minutes: 20 - seconds: 0 - milliseconds: 0 - - action: notify.mobile_app_sm_a528b - metadata: {} - data: - message: 'Solaranlage wurde neugestartet. Bitte prüfen. ' - mode: single -- id: '1731005590680' - alias: Williams Nachtlicht - description: '' - triggers: - - type: turned_on - device_id: cad43afeeb57fdb2fdc91e9e516aafb9 - entity_id: 42a0373b9929efa79f75b4e3c57935db - domain: light - trigger: device - for: - hours: 0 - minutes: 10 - seconds: 0 - conditions: - - condition: time - after: '22:00:00' - before: 06:00:00 - weekday: - - mon - - tue - - wed - - thu - - fri - - sat - - sun - actions: - - type: turn_off - device_id: cad43afeeb57fdb2fdc91e9e516aafb9 - entity_id: 42a0373b9929efa79f75b4e3c57935db - domain: light -- id: '2323' - mode: single - alias: Thermostat zurücksetzen nach manuellem Eingriff - description: Nach einem manuellen Eingriff am Thermostat wird nach 30 Minuten die - geplante Temperatur wiederhergestellt. - trigger: - - platform: state - entity_id: climate.angelina_heizung - attribute: hvac_action - to: heat - action: - - delay: 00:30:00 - - service: climate.set_temperature - target: - entity_id: climate.angelina_heizung - data: - temperature: '{{ states(''input_number.geplante_temperatur_angelina'') }}' - hvac_mode: heat -- id: '1735397907525' - alias: Angelina Heizt zu lange - description: '' - triggers: - - device_id: 3abfd883b327cb2ce142326c0a3bc28b - domain: climate - entity_id: c9bfc7b59e5413bab16257523e9171c9 - type: hvac_mode_changed - trigger: device - to: heat - for: - hours: 0 - minutes: 30 - seconds: 0 - conditions: [] - actions: - - device_id: 3abfd883b327cb2ce142326c0a3bc28b - domain: number - entity_id: ee73df6c7d26e1e8b55622651b3b556e - type: set_value - value: 18.4 - mode: single -- id: '1736232254787' - alias: Stromzähler um 0 Uhr wegschreiben - description: '' - triggers: - - trigger: time - at: 00:00:00 - conditions: [] - actions: - - action: input_number.set_value - metadata: {} - data: - value: '{{ states(''sensor.stromzahler_energieverbrauch'') | float }}' - target: - entity_id: input_number.tagesstart_zahlerstand - mode: single -- id: '1736571405638' - alias: Handy Flugmodus - description: '' - triggers: - - trigger: time - at: '23:00:00' - conditions: [] - actions: [] - mode: single -- id: '1736571405538' - alias: Manuelle Temperaturänderung zurücksetzen - trigger: - - platform: state - entity_id: climate.angelina_heizung_neu - attribute: temperature - condition: - - condition: template - value_template: '{{ trigger.from_state.state != trigger.to_state.state }} # Zieltemperatur - hat sich geändert - - ' - - condition: template - value_template: '{{ (as_timestamp(state_attr(''scheduler.slot'', ''next_trigger'')) - - now().timestamp()) > 1800 }} # Nur auslösen, wenn die nächste Scheduler-Aktion - mehr als 30 Minuten entfernt ist - - ' - action: - - delay: 00:30:00 - - service: climate.set_temperature - target: - entity_id: climate.angelina_heizung_neu - data: - temperature: '{{ state_attr(''scheduler.slot'', ''current_slot'') }}' -- id: '1737549179058' - alias: Williams Licht ausschalten - description: '' - triggers: - - trigger: time - at: '21:05:00' - conditions: [] - actions: - - type: turn_off - device_id: cad43afeeb57fdb2fdc91e9e516aafb9 - entity_id: 42a0373b9929efa79f75b4e3c57935db - domain: light - mode: single -- id: '1741251645491' - alias: Waschmaschine läuft - description: '' - use_blueprint: - path: sbyx/notify-or-do-something-when-an-appliance-like-a-dishwasher-or-washing-machine-finishes.yaml - input: - power_sensor: sensor.waschmaschine_leistung - starting_threshold: 3 - actions: - - action: notify.mobile_app_sm_a528b - metadata: {} - data: - title: Waschmaschine ist fertig - message: -> Bitte Aufhängen ❤️ - - action: notify.mobile_app_pixel_7_pro - metadata: {} - data: - title: Waschmaschine ist fertig - message: -> Bitte Aufhängen ❤️ - pre_actions: - - action: notify.mobile_app_sm_a528b - metadata: {} - data: - message: Waschmaschine läuft - starting_hysteresis: 0.25 - finishing_hysteresis: 3 -- id: '12123' - alias: Müllabholung Erinnerung - description: Erinnert am Vorabend an die Müllabholung - triggers: - - trigger: time - at: '18:00:00' - conditions: - - condition: template - value_template: '{{ states(''sensor.nachste_mullabholung'').split('' - '')[1] - }} - - ' - actions: - - action: notify.mobile_app_sm_a528b - data: - title: Müllabholung Erinnerung - message: Morgen wird {{ states('sensor.nachste_muellabholung').split(' - ')[1] - }} abgeholt. Stell die Tonne raus! - mode: single -- id: '1741424638859' - alias: Müllsensor aktualisieren - description: '' - triggers: - - trigger: time_pattern - minutes: '10' - conditions: [] - actions: - - action: homeassistant.update_entity - metadata: {} - data: - entity_id: - - sensor.nachste_mullabholung - mode: single -- id: '12123123' - alias: Start Aktiv-Timer bei > 50 W - trigger: - - platform: numeric_state - entity_id: sensor.kuehlschrank_power - above: 50 - action: - - service: input_datetime.set_datetime - data: - entity_id: input_datetime.kuehlschrank_ende_aktiv - time: '{% set duration = states(''sensor.kuehlschrank_durchschnitt_aktivzeit'')|int - %} {{ (now() + timedelta(minutes=duration)).time() }} - - ' -- id: '12123124' - alias: Start Pause-Timer bei < 50 W - trigger: - - platform: numeric_state - entity_id: sensor.kuehlschrank_power - below: 50 - action: - - service: input_datetime.set_datetime - data: - entity_id: input_datetime.kuehlschrank_ende_pause - time: '{% set duration = states(''sensor.kuehlschrank_durchschnitt_pausezeit'')|int - %} {{ (now() + timedelta(minutes=duration)).time() }} - - ' -- id: '1742075999049' - alias: Essbereich statt Esslicht anschalten - description: '' - triggers: - - type: changed_states - device_id: 4f3c4880f738eee7216f506150f812fc - entity_id: 218df72843fc909631db8c91cefd22a9 - domain: light - trigger: device - conditions: - - condition: device - type: is_off - device_id: 6dfb101cd1340498118e6fe88ed4aeda - entity_id: 4152920c27fdff05870052d95ffed477 - domain: light - actions: - - type: turn_off - device_id: 4f3c4880f738eee7216f506150f812fc - entity_id: 218df72843fc909631db8c91cefd22a9 - domain: light - - type: turn_on - device_id: 6dfb101cd1340498118e6fe88ed4aeda - entity_id: 4152920c27fdff05870052d95ffed477 - domain: light - mode: single -- id: '1742076042831' - alias: Essbereich statt Esslicht ausschalten - description: '' - triggers: - - type: changed_states - device_id: 4f3c4880f738eee7216f506150f812fc - entity_id: 218df72843fc909631db8c91cefd22a9 - domain: light - trigger: device - conditions: - - condition: device - type: is_on - device_id: 6dfb101cd1340498118e6fe88ed4aeda - entity_id: 4152920c27fdff05870052d95ffed477 - domain: light - actions: - - type: turn_off - device_id: 4f3c4880f738eee7216f506150f812fc - entity_id: 218df72843fc909631db8c91cefd22a9 - domain: light - - type: turn_off - device_id: 6dfb101cd1340498118e6fe88ed4aeda - entity_id: 4152920c27fdff05870052d95ffed477 - domain: light - mode: single -- id: '1742564448586' - alias: 20% Schutz Nachts einschalten - description: '' - triggers: - - trigger: time - at: 00:00:00 - conditions: - - condition: state - entity_id: automation.akku_muss_laden - state: 'off' - actions: - - action: automation.turn_on - target: - entity_id: automation.akku_muss_laden - data: {} - mode: single -- id: '1748352904414' - alias: Staubsauger fertig geladen - description: '' - triggers: - - trigger: numeric_state - entity_id: - - vacuum.dreamebot_l10s_ultra - - sensor.dreamebot_l10s_ultra_battery_level - above: 99 - conditions: [] - actions: - - action: notify.mobile_app_sm_a528b - metadata: {} - data: - message: Staubsauger ist fertig aufgeladen - mode: single -- id: '1749392758104' - alias: Wechselrichter kühlen An - description: '' - triggers: - - trigger: numeric_state - entity_id: - - sensor.balkonkraftwerk_temperatur - above: 42 - conditions: [] - actions: - - type: turn_on - device_id: b2cdf7ba0db6113b10cf139a6dbc09e1 - entity_id: 98c502f8c6675178649b5f175ca31b72 - domain: switch - mode: single -- id: '1749392810222' - alias: Wechselrichter kühlen aus - description: '' - triggers: - - trigger: numeric_state - entity_id: - - sensor.balkonkraftwerk_temperatur - below: 33 - conditions: [] - actions: - - type: turn_off - device_id: b2cdf7ba0db6113b10cf139a6dbc09e1 - entity_id: 98c502f8c6675178649b5f175ca31b72 - domain: switch - mode: single diff --git a/HA_configuration.yaml b/HA_configuration.yaml deleted file mode 100644 index 26b58fdd..00000000 --- a/HA_configuration.yaml +++ /dev/null @@ -1,667 +0,0 @@ -# Loads default set of integrations. Do not remove. -default_config: - -# Load frontend themes from the themes folder -frontend: - themes: !include_dir_merge_named themes - -# Text to speech -tts: - - platform: google_translate - -automation: !include automations.yaml -script: !include scripts.yaml -scene: !include scenes.yaml - -#Anleitung: https://book.cryd.de/books/projekte/page/hausverbrauch-strom-messen-incl-dummy-sensoren - -utility_meter: - daily_upload_volume: - source: sensor.fritzbox_upload_volumen - cycle: daily - daily_download_volume: - source: sensor.fritzbox_download_volumen - cycle: daily - taeglicher_stromverbrauch: - source: sensor.stromzahler_energieverbrauch - cycle: daily - taegliche_einspeisung: - source: sensor.stromzahler_energieeinspeisung - cycle: daily - -input_boolean: - manual_trigger: - name: Manual Trigger - initial: off - -influxdb: - host: 127.0.0.1 - #host: a0d7b954-influxdb - port: 8086 - database: homeassistant - username: !secret influxdb_user - password: !secret influxdb_pw - max_retries: 3 - default_measurement: state - -alexa: - smart_home: - endpoint: https://api.eu.amazonalexa.com/v3/events - filter: - include_entities: - - light.living_room - - switch.kitchen - entity_config: - light.living_room: - name: "Wohnzimmer Licht" - switch.kitchen: - name: "Küchenschalter" -template: - - sensor: - - name: "Total Power3" - unique_id: "total_power_sensor3" - unit_of_measurement: "W" - device_class: power - state_class: measurement - state: > - {{ - states('sensor.shelly_em3_channel_a_power') | float(0) + - states('sensor.shelly_em3_channel_b_power') | float(0) + - states('sensor.shelly_em3_channel_c_power') | float(0) - }} - - name: "Prozent Nutzung" - unique_id: "pv_prozent_nutzung" - unit_of_measurement: "%" - state: > - {% set total_power = states('sensor.total_power_v2') | float(0) + states('sensor.solaranlage_power') | float(0) %} - {% if total_power > 0 %} - {{ (100 * states('sensor.solaranlage_power') | float(0) / total_power) | round(1) }} - {% else %} - 0 - {% endif %} - - name: "Total Energy Use1" - unique_id: "total_energy_use1" - device_class: energy - state_class: total_increasing - unit_of_measurement: "kWh" - state: > - {{ - states('sensor.shelly_em3_channel_a_energy') | float(0) + - states('sensor.shelly_em3_channel_b_energy') | float(0) + - states('sensor.shelly_em3_channel_c_energy') | float(0) - }} - - name: "Total Energy Returned1" - unique_id: "total_energy_returned1" - device_class: energy - state_class: total_increasing - unit_of_measurement: "kWh" - state: > - {{ - states('sensor.shelly_em3_channel_a_energy_returned') | float(0) + - states('sensor.shelly_em3_channel_b_energy_returned') | float(0) + - states('sensor.shelly_em3_channel_c_energy_returned') | float(0) - }} - - name: "Aktuelle Solarleistung1" - unique_id: "aktuelle_solarleistung1" - unit_of_measurement: "W" - device_class: power - state_class: measurement - state: > - {{ - max(0, states('sensor.esphome_web_39b3f0_charging_power_2') | float(0) - - states('sensor.esphome_web_39b3f0_discharging_power_2') | float(0) + - states('sensor.solaranlage_power') | float(0)) - }} - - name: "Täglicher Stromverbrauch" - unit_of_measurement: "kWh" - state: > - {% set aktueller_wert = states('sensor.stromzahler_energieverbrauch') | float %} - {% set startwert = states('input_number.tagesstart_zaehlerstand') | float %} - {{ (aktueller_wert - startwert) | round(2) }} - - name: "Fritzbox Download Volumen" - unit_of_measurement: "MB" - state: > - {% set rate_kbps = states('sensor.fritz_box_7530_download_durchsatz') | float %} - {% set rate_kBps = rate_kbps / 8 %} # Kilobits pro Sekunde in Kilobytes umrechnen - {{ (rate_kBps * 60) / 1024 }} # Datenvolumen pro Minute in Megabyte - - name: "Fritzbox Upload Volumen" - unit_of_measurement: "MB" - state: > - {% set rate_kbps = states('sensor.fritz_box_7530_upload_durchsatz') | float %} - {% set rate_kBps = rate_kbps / 8 %} # Kilobits pro Sekunde in Kilobytes umrechnen - {{ (rate_kBps * 60) / 1024 }} # Datenvolumen pro Minute in Megabyte - - name: "Aktueller Strompreis" - state: "{{ states('input_number.strompreis') }}" - unit_of_measurement: "€/kWh" - device_class: monetary - - name: "Stromverbrauch Vortag" - unique_id: "stromverbrauch_vortag" - unit_of_measurement: "kWh" - device_class: energy - state: > - {% set stats = state_attr('sensor.taeglicher_stromverbrauch', 'last_period') %} - {{ stats | float(0) }} - - name: "Einspeisung Vortag" - unique_id: "einspeisung_vortag" - unit_of_measurement: "kWh" - device_class: energy - state: > - {% set stats = state_attr('sensor.taegliche_einspeisung', 'last_period') %} - {{ stats | float(0) }} - - name: "Generiert Vortag (Template)" - unique_id: "generiert_vortag_template" - unit_of_measurement: "kWh" - device_class: energy - state: > - {% set stats = state_attr('sensor.komplett_solarlieferung', 'last_period') %} - {{ stats | float(0) }} - - name: "Nächste Müllabholung" - state: >- - {% set today = now().date().isoformat() %} - {% for date in states.sensor.garbage.attributes.keys() | list | sort %} - {% if date >= today %} - {{ date }} - {{ states.sensor.garbage.attributes[date] }} - {% break %} - {% endif %} - {% endfor %} - - name: "Statistik Solarerzeugung Durchschnitt" - state: "{{ now().year }}" - attributes: - data: > - {{ states('sensor.gsheet_data') }} - - name: "Solarertrag 2022" - state: "OK" - attributes: - values: > - {% set raw_data = state_attr('sensor.statistik_solarerzeugung_durchschnitt_mqtt', 'data')[1][1:] %} - {{ raw_data | map('replace', ',', '.') | map('float') | list }} - - name: "Solarertrag 2023" - state: "OK" - attributes: - values: > - {% set raw_data = state_attr('sensor.statistik_solarerzeugung_durchschnitt_mqtt', 'data')[2][1:] %} - {{ raw_data | map('replace', ',', '.') | map('float') | list }} - - name: "Solarertrag 2024" - state: "OK" - attributes: - values: > - {% set raw_data = state_attr('sensor.statistik_solarerzeugung_durchschnitt_mqtt', 'data')[3][1:] %} - {{ raw_data | map('replace', ',', '.') | map('float') | list }} - - name: "Solarertrag 2025" - state: "OK" - attributes: - values: > - {% set raw_data = state_attr('sensor.statistik_solarerzeugung_durchschnitt_mqtt', 'data')[4][1:] %} - {{ raw_data | map('replace', ',', '.') | map('float') | list }} - - name: "Solarertrag 2022 Werte" - state: "{{ state_attr('sensor.solarertrag_2022', 'values')[-1] | float(0) }}" - unit_of_measurement: "kWh" # Passen Sie die Einheit an - state_class: measurement - attributes: - alle_werte: "{{ state_attr('sensor.solarertrag_2022', 'values') }}" - - - name: "Kühlschrank Letzte Aktivzeit" - unique_id: kuehlschrank_letzte_aktivzeit - unit_of_measurement: "min" - state: > - {% set aktiv_start = states.binary_sensor.kuehlschrank_laeuft.last_changed %} - {% if is_state('binary_sensor.kuehlschrank_laeuft', 'on') %} - {{ ((now() - aktiv_start).total_seconds() / 60) | round(1) }} - {% else %} - 0 - {% endif %} - - - name: "Kühlschrank Letzte Pausezeit" - unique_id: kuehlschrank_letzte_pausezeit - unit_of_measurement: "min" - state: > - {% set pause_start = states.binary_sensor.kuehlschrank_laeuft.last_changed %} - {% if is_state('binary_sensor.kuehlschrank_laeuft', 'off') %} - {{ ((now() - pause_start).total_seconds() / 60) | round(1) }} - {% else %} - 0 - {% endif %} -sensor: - - platform: average - name: "Durchschnittsverbrauch" - unique_id: "durchschnitt_verbrauch" - duration: 60 - entities: - - sensor.total_power_v2 - - - platform: average - name: "Durchschnittsertrag" - unique_id: "durchschnitt_ertrag" - duration: 180 - entities: - - sensor.aktuelle_solarleistung1 - - - platform: teamtracker - league_id: "BUND" - team_id: "MUC" - name: "Bayern2" - - - platform: integration - name: Upload Volume - source: sensor.fritz_box_7530_upload_durchsatz - unit_prefix: k - round: 2 - - platform: integration - name: Download Volume - source: sensor.fritz_box_7530_download_durchsatz - unit_prefix: k - round: 2 - - platform: statistics - name: "Generiert Vortag (Statistik)" - entity_id: sensor.solaranlage_energy - state_characteristic: change - max_age: - days: 1 - - platform: rest - name: "Google Sheets Daten" - resource: "https://script.google.com/macros/s/AKfycbz4sAiMvufOqL-gv5o7YfjaL4V0eWu9dGren_xg6pV35dE8bMyzaQckKp5WCs6ex5bbdA/exec" - scan_interval: 600 # Aktualisiert alle 10 Minuten - value_template: "{{ value_json[0] }}" # Falls erforderlich, kann dies angepasst werden - json_attributes: - - "Gesamtwerte" - - "Genergiert" - - "Einspeisung" - - "Netzverbrauch" - - "Solarverbrauch" - - "Gesamtverbrauch" - - "Durchschnittl. Nutzung" - - "Autarkiegrad" - - "Ersparnis in € / Tag" - - "Ersparnis gesamt" - - "Prozent Abgezahlt" - - "Gesamnt abgezahlt" - - platform: history_stats - name: "Kühlschrank Aktivzeit" - entity_id: binary_sensor.kuehlschrank_laeuft - state: "on" - type: time - start: "{{ now() - timedelta(hours=24) }}" - end: "{{ now() }}" - - - platform: history_stats - name: "Kühlschrank Pausezeit" - entity_id: binary_sensor.kuehlschrank_laeuft - state: "off" - type: time - start: "{{ now() - timedelta(hours=24) }}" - end: "{{ now() }}" - - platform: statistics - name: "Kühlschrank Durchschnitt Aktivzeit" - entity_id: sensor.kuehlschrank_letzte_aktivzeit - state_characteristic: mean - max_age: - hours: 24 - sampling_size: 10 - - - platform: statistics - name: "Kühlschrank Durchschnitt Pausezeit" - entity_id: sensor.kuehlschrank_letzte_pausezeit - state_characteristic: mean - max_age: - hours: 24 - sampling_size: 10 - -input_datetime: - kuehlschrank_ende_aktiv: - name: "Ende aktive Phase" - has_time: true - kuehlschrank_ende_pause: - name: "Ende Pause Phase" - has_time: true - -waste_collection_schedule: - sources: - - name: awido_de - args: - customer: Erding - city: Oberding - street: "Gartenstraße" - -binary_sensor: - - platform: template - sensors: - kuehlschrank_laeuft: - friendly_name: "Kühlschrank läuft" - value_template: "{{ states('sensor.kuehlschrank_power')|float > 50 }}" - -mqtt: - sensor: - - name: "Balkonkraftwerk Leistung AC" - state_topic: "inverter/hm600/ch0/P_AC" - device_class: power - unit_of_measurement: W - state_class: measurement - unique_id: "BalkonkraftwerkLeistungAC" - - name: "Balkonkraftwerk Module 1 Leistung" - state_topic: "inverter/hm600/ch1/P_DC" - device_class: power - unit_of_measurement: W - state_class: measurement - unique_id: "BalkonkraftwerkModule13Leistung" - - name: "Balkonkraftwerk Module 2 Leistung" - state_topic: "inverter/hm600/ch2/P_DC" - device_class: power - unit_of_measurement: W - state_class: measurement - unique_id: "BalkonkraftwerkModule24Leistung" - - name: "Balkonkraftwerk Temperatur" - state_topic: "inverter/hm600/ch0/Temp" - device_class: temperature - unit_of_measurement: °C - state_class: measurement - unique_id: "BalkonkraftwerkTemperatur" - - name: "Balkonkraftwerk Arbeit Tag" - state_topic: "inverter/hm600/ch0/YieldDay" - device_class: energy - unit_of_measurement: Wh - state_class: total_increasing - unique_id: "BalkonkraftwerkArbeitTag" - - name: "Balkonkraftwerk Arbeit Gesamt" - state_topic: "inverter/hm600/ch0/YieldTotal" - device_class: energy - unit_of_measurement: kWh - state_class: total_increasing - unique_id: "BalkonkraftwerkArbeitGesamt" - - name: "version" - state_topic: "inverter/version" - unique_id: "version_dtu" - - name: "Limit" - state_topic: "inverter/hm600/ch0/active_PowerLimit" - unique_id: "set_powerlimit" - - name: "Energy Akkuentladung current" - device_class: power - unit_of_measurement: "W" - state_topic: "esphome-web-39b3f0/sensor/esphome-web-39b3f0_discharging_power" - unique_id: "energy_akkuentladung" - - name: "Energy Akkuentladung total" - device_class: energy - unit_of_measurement: "kWh" - state_topic: "esphome-web-39b3f0/sensor/esphome-web-39b3f0_discharging_power" - - name: "Effizienz HM600" - unit_of_measurement: "%" - state_topic: "inverter/hm600/ch0/Efficiency" - unique_id: "effizienz_hm600" - - name: "HM600 Spannung" - unit_of_measurement: "V" - state_topic: "inverter/hm600/ch1/U_DC" - - name: "Waschmaschine Leistung" - state_topic: "shellyplus1pm-84cca8771670/status/switch:0" - value_template: "{{ value_json.apower }}" - unit_of_measurement: "W" - device_class: power - - name: "Waschmaschine Energieverbrauch" - state_topic: "shellyplus1pm-84cca8771670/status/switch:0" - value_template: "{{ value_json.aenergy.total }}" - unit_of_measurement: "kWh" - device_class: energy - - name: "Statistik Solarerzeugung Durchschnitt mqtt" - state_topic: "homeassistant/sensor/gsheet_data" - value_template: "{{ value_json.state }}" - json_attributes_topic: "homeassistant/sensor/gsheet_data" - json_attributes_template: "{{ value_json.attributes | tojson }}" - -logger: - default: warning - logs: - custom_components.awtrix: warning - homeassistant.components.sensor: warning - -# Nächste Abholung Restmüll - -# - name: "Restmüll" -# state: '{{value.types|join(", ")}}{% if value.daysTo == 0 %} Heute{% elif value.daysTo == 1 %} Morgen{% else %} in {{value.daysTo}} Tagen{% endif %}' -# attributes: -# value_template: '{{value.types|join(", ")}}' -# unique_id: "restmuell" -# unit_of_measurement: "days" -# device_class: "timestamp" -# value_template: '{{(states.sensor.waste_collection_schedule.attributes.next_date)|as_timestamp | timestamp_local}}' - -# Nächste Abholung Biotonne - -# - name: "Biotonne" -# state: '{{value.types|join(", ")}}{% if value.daysTo == 0 %} Heute{% elif value.daysTo == 1 %} Morgen{% else %} in {{value.daysTo}} Tagen{% endif %}' -# attributes: -# value_template: '{{value.types|join(", ")}}' -# unique_id: "biotonne" -# unit_of_measurement: "days" -# device_class: "timestamp" -# value_template: '{{(states.sensor.waste_collection_schedule.attributes.next_date)|as_timestamp | timestamp_local}}' - -##sensor: -# - platform: average -# name: 'Durchschnittsverbrauch' -# unique_id: 'durchschnitt_verbrauch' -# duration: 60 -# entities: -# - sensor.total_power -# - platform: average -# name: 'Durchschnittsertrag' -# unique_id: 'durchschnitt_ertrag' -# duration: 180 -# entities: -# - sensor.aktuelle_solarleistung -# - platform: teamtracker -# league_id: "BUND" -# team_id: "MUC" -# name: "Bayern2" -# -# - platform: template -# name: "Total Power" -# unique_id: "Total_Energy" -# device_class: power -# state_class: total -# unit_of_measurement: "W" -# value_template: > -# {{ -# states('sensor.shelly_em3_channel_a_power')| float(0) + -# states('sensor.shelly_em3_channel_b_power')| float(0) + -# states('sensor.shelly_em3_channel_c_power')| float(0) -# }} -# -# - platform: template -# name: "Total Energy Use1" -# unique_id: "Total_Energy_Use1" -# device_class: energy -# state_class: total -# unit_of_measurement: "kWh" -# value_template: > -# {{ -# states('sensor.shelly_em3_channel_a_energy')| float(0) + -# states('sensor.shelly_em3_channel_b_energy')| float(0) + -# states('sensor.shelly_em3_channel_c_energy')| float(0) -# }} -# -# - name: "Total Energy Returned1" -# unique_id: "Total_Energy_Returned1" -# device_class: energy -# state_class: total -# unit_of_measurement: "kWh" -# value_template: > -# {{ -# states('sensor.shelly_em3_channel_a_energy_returned')| float(0) + -# states('sensor.shelly_em3_channel_b_energy_returned')| float(0) + -# states('sensor.shelly_em3_channel_c_energy_returned')| float(0) -# }} -# -# - name: "PV Einspeisung" -# unique_id: "pv_einspeisung" -# unit_of_measurement: "W" -# device_class: power -# value_template: "{{ states('sensor.total_power')|float if states('sensor.total_power') | int < 1 else 0 }}" -# -# - name: "PV Einspeisung negiert" -# unique_id: "pv_einspeisung_negiert" -# unit_of_measurement: "W" -# device_class: power -# value_template: "{{ states('sensor.pv_einspeisung')|float * -1 }}" -# -# - name: "Wirkungsgrad" -# unique_id: "wirkungsgrad_battery" -# unit_of_measurement: "%" -# device_class: power -# value_template: > -# {{(100 * states('sensor.solaranlage_power')| float(0) / states('sensor.esphome_web_39b3f0_discharging_power')| float(0)) | round(1) }} -# -# - name: "Prozent_Nutzung" -# unique_id: "pv_prozent_nutzung" -# unit_of_measurement: "%" -# device_class: power -# value_template: > -# {{ -# (100 * states('sensor.solaranlage_power')| float(0) / (states('sensor.solaranlage_power')| float(0) + states('sensor.total_power_v2')| float(0))) | round(1) -# }} -# -# - name: "Aktuelle_Solarleistung" -# unique_id: "aktuelle-solarleistung" -# unit_of_measurement: "W" -# device_class: power -# value_template: > -# {{ -# max(0, states('sensor.esphome_web_39b3f0_charging_power_2')| float(0) - -# states('sensor.esphome_web_39b3f0_discharging_power_2')| float(0) + -# states('sensor.solaranlage_power')|float(0) + -# }} -# -# //states('sensor.akku_power')|float(0)) removed from aktuelle solarleistung -# -# - name: "Summierter Ertrag" -# unique_id: "summierter_ertrag" -# unit_of_measurement: "W" -# device_class: power -# value_template: > -# {{ -# states('sensor.akku_power')| float(0) + -# states('sensor.solaranlage_power')|float(0) -# }} -# -# - name: "Total Power" -# unique_id: "Total_Energy" -# device_class: power -# state_class: total -# unit_of_measurement: "W" -# value_template: > -# {{ -# states('sensor.shelly_em3_channel_a_power')| float(0) + -# states('sensor.shelly_em3_channel_b_power')| float(0) + -# states('sensor.shelly_em3_channel_c_power')| float(0) -# }} -# -# - name: "Total Energy Use" -# unique_id: "Total_Energy_Use" -# device_class: energy -# state_class: total -# unit_of_measurement: "kWh" -# value_template: > -# {{ -# states('sensor.shelly_em3_channel_a_energy')| float(0) + -# states('sensor.shelly_em3_channel_b_energy')| float(0) + -# states('sensor.shelly_em3_channel_c_energy')| float(0) -# }} -# -# - name: "Total Energy Returned" -# unique_id: "Total_Energy_Returned" -# device_class: energy -# state_class: total -# unit_of_measurement: "kWh" -# value_template: > -# {{ -# states('sensor.shelly_em3_channel_a_energy_returned')| float(0) + -# states('sensor.shelly_em3_channel_b_energy_returned')| float(0) + -# states('sensor.shelly_em3_channel_c_energy_returned')| float(0) -# }} -# -# - name: "PV Einspeisung" -# unique_id: "pv_einspeisung" -# unit_of_measurement: "W" -# device_class: power -# value_template: "{{ states('sensor.total_power')|float if states('sensor.total_power') | int < 1 else 0 }}" -# -# - name: "PV Einspeisung negiert" -# unique_id: "pv_einspeisung_negiert" -# unit_of_measurement: "W" -# device_class: power -# value_template: "{{ states('sensor.pv_einspeisung')|float * -1 }}" -# -# - name: "Wirkungsgrad" -# unique_id: "wirkungsgrad_battery" -# unit_of_measurement: "%" -# device_class: power -# value_template: > -# {{(100 * states('sensor.solaranlage_power')| float(0) / states('sensor.esphome_web_39b3f0_discharging_power')| float(0)) | round(1) }}### -# -# - name: "Prozent_Nutzung" -# unique_id: "pv_prozent_nutzung" -# unit_of_measurement: "%" -# device_class: power -# value_template: > -# {{ -# (100 * states('sensor.solaranlage_power')| float(0) / (states('sensor.solaranlage_power')| float(0) + states('sensor.total_power')| float(0))) | round(1) -# }} -# -# - name: "Aktuelle_Solarleistung" -# unique_id: "aktuelle-solarleistung" -# unit_of_measurement: "W" -# device_class: power -# value_template: > -# {{ -# max(0, states('sensor.esphome_web_39b3f0_charging_power_2')| float(0) - -# states('sensor.esphome_web_39b3f0_discharging_power_2')| float(0) + -# states('sensor.solaranlage_power')|float(0) + -# states('sensor.akku_power')|float(0)) -# }} -# -# - name: "Summierter Ertrag" -# unique_id: "summierter_ertrag" -# unit_of_measurement: "W" -# device_class: power -# value_template: > -# {{ -# states('sensor.akku_power')| float(0) + -# states('sensor.solaranlage_power')|float(0) -# }} - -# https://community.home-assistant.io/t/hoymiles-dtu-microinverters-pv/253674/21 - -# statistics -#- platform: statistics -# entity_id: sensor.total_power_av -# sampling_size: 20 - -#powercalc: - -#sensor: -# - platform: powercalc -# entity_id: light.esslicht -# fixed: -# states_power: -# off: 0.4 -# on: 22 - -# platform: template -# daily_solar_percent: -# value_template: "{{ ( 100 * states('sensor.total_power')|float / states('sensor.solaranlage_power')|float )|round(1) }}" -# unit_of_measurement: '%' -# friendly_name: Daily Solar Percentage - -# ssl Configuration -# http: -# ssl_certificate: /ssl/fullchain.pem -# ssl_key: /ssl/privkey.pem - -http: - use_x_forwarded_for: true - trusted_proxies: - - 127.0.0.1 - - 192.168.178.6 - - 172.16.0.0/12 - - ::1 - #ssl_certificate: "/ssl/fullchain.pem" - #ssl_key: "/ssl/privkey.pem" -homeassistant: - external_url: "https://floke-ha.duckdns.org" diff --git a/HA_jbd_bms.yaml b/HA_jbd_bms.yaml deleted file mode 100644 index c0360d06..00000000 --- a/HA_jbd_bms.yaml +++ /dev/null @@ -1,260 +0,0 @@ -substitutions: - name: esphome-web-39b3f0 - device_description: "Monitor and control a Xiaoxiang Battery Management System (JBD-BMS) via BLE" - external_components_source: github://syssi/esphome-jbd-bms@main - mac_address: A4:C1:37:00:86:5A - -esphome: - name: ${name} - comment: ${device_description} - min_version: 2024.6.0 - project: - name: "syssi.esphome-jbd-bms" - version: 2.1.0 - -esp32: - board: esp32dev - framework: - type: esp-idf - -external_components: - - source: ${external_components_source} - refresh: 0s - -wifi: - ssid: !secret wifi_ssid - password: !secret wifi_password - -ota: - platform: esphome - -logger: - level: DEBUG - -# If you use Home Assistant please remove this `mqtt` section and uncomment the `api` component! -# The native API has many advantages over MQTT: https://esphome.io/components/api.html#advantages-over-mqtt -#mqtt: -# broker: !secret mqtt_host -# username: !secret mqtt_username -# password: !secret mqtt_password -# id: mqtt_client - -# api: - -esp32_ble_tracker: - scan_parameters: - active: false - -ble_client: - - id: client0 - mac_address: ${mac_address} - -jbd_bms_ble: - - id: bms0 - ble_client_id: client0 - # Some Liontron BMS models require an update interval of less than 8s - update_interval: 2s - -button: - - platform: jbd_bms_ble - jbd_bms_ble_id: bms0 - retrieve_hardware_version: - name: "${name} retrieve hardware version" - force_soc_reset: - name: "${name} force soc reset" - -binary_sensor: - - platform: jbd_bms_ble - jbd_bms_ble_id: bms0 - balancing: - name: "${name} balancing" - charging: - name: "${name} charging" - discharging: - name: "${name} discharging" - online_status: - name: "${name} online status" - -sensor: - - platform: jbd_bms_ble - jbd_bms_ble_id: bms0 - battery_strings: - name: "${name} battery strings" - current: - name: "${name} current" - power: - name: "${name} power" - charging_power: - name: "${name} charging power" - discharging_power: - name: "${name} discharging power" - state_of_charge: - name: "${name} state of charge" - nominal_capacity: - name: "${name} nominal capacity" - charging_cycles: - name: "${name} charging cycles" - capacity_remaining: - name: "${name} capacity remaining" - battery_cycle_capacity: - name: "${name} battery cycle capacity" - total_voltage: - name: "${name} total voltage" - average_cell_voltage: - name: "${name} average cell voltage" - delta_cell_voltage: - name: "${name} delta cell voltage" - min_cell_voltage: - name: "${name} min cell voltage" - max_cell_voltage: - name: "${name} max cell voltage" - min_voltage_cell: - name: "${name} min voltage cell" - max_voltage_cell: - name: "${name} max voltage cell" - temperature_1: - name: "${name} temperature 1" - temperature_2: - name: "${name} temperature 2" - temperature_3: - name: "${name} temperature 3" - temperature_4: - name: "${name} temperature 4" - temperature_5: - name: "${name} temperature 5" - temperature_6: - name: "${name} temperature 6" - cell_voltage_1: - name: "${name} cell voltage 1" - cell_voltage_2: - name: "${name} cell voltage 2" - cell_voltage_3: - name: "${name} cell voltage 3" - cell_voltage_4: - name: "${name} cell voltage 4" - cell_voltage_5: - name: "${name} cell voltage 5" - cell_voltage_6: - name: "${name} cell voltage 6" - cell_voltage_7: - name: "${name} cell voltage 7" - cell_voltage_8: - name: "${name} cell voltage 8" - cell_voltage_9: - name: "${name} cell voltage 9" - cell_voltage_10: - name: "${name} cell voltage 10" - cell_voltage_11: - name: "${name} cell voltage 11" - cell_voltage_12: - name: "${name} cell voltage 12" - cell_voltage_13: - name: "${name} cell voltage 13" - cell_voltage_14: - name: "${name} cell voltage 14" - cell_voltage_15: - name: "${name} cell voltage 15" - cell_voltage_16: - name: "${name} cell voltage 16" - cell_voltage_17: - name: "${name} cell voltage 17" - cell_voltage_18: - name: "${name} cell voltage 18" - cell_voltage_19: - name: "${name} cell voltage 19" - cell_voltage_20: - name: "${name} cell voltage 20" - cell_voltage_21: - name: "${name} cell voltage 21" - cell_voltage_22: - name: "${name} cell voltage 22" - cell_voltage_23: - name: "${name} cell voltage 23" - cell_voltage_24: - name: "${name} cell voltage 24" - cell_voltage_25: - name: "${name} cell voltage 25" - cell_voltage_26: - name: "${name} cell voltage 26" - cell_voltage_27: - name: "${name} cell voltage 27" - cell_voltage_28: - name: "${name} cell voltage 28" - cell_voltage_29: - name: "${name} cell voltage 29" - cell_voltage_30: - name: "${name} cell voltage 30" - cell_voltage_31: - name: "${name} cell voltage 31" - cell_voltage_32: - name: "${name} cell voltage 32" - operation_status_bitmask: - name: "${name} operation status bitmask" - errors_bitmask: - name: "${name} errors bitmask" - balancer_status_bitmask: - name: "${name} balancer status bitmask" - software_version: - name: "${name} software version" - short_circuit_error_count: - name: "${name} short circuit error count" - charge_overcurrent_error_count: - name: "${name} charge overcurrent error count" - discharge_overcurrent_error_count: - name: "${name} discharge overcurrent error count" - cell_overvoltage_error_count: - name: "${name} cell overvoltage error count" - cell_undervoltage_error_count: - name: "${name} cell undervoltage error count" - charge_overtemperature_error_count: - name: "${name} charge overtemperature error count" - charge_undertemperature_error_count: - name: "${name} charge undertemperature error count" - discharge_overtemperature_error_count: - name: "${name} discharge overtemperature error count" - discharge_undertemperature_error_count: - name: "${name} discharge undertemperature error count" - battery_overvoltage_error_count: - name: "${name} battery overvoltage error count" - battery_undervoltage_error_count: - name: "${name} battery undervoltage error count" - -text_sensor: - - platform: jbd_bms_ble - jbd_bms_ble_id: bms0 - errors: - name: "${name} errors" - operation_status: - name: "${name} operation status" - device_model: - name: "${name} device model" - -select: - - platform: jbd_bms_ble - jbd_bms_ble_id: bms0 - read_eeprom_register: - name: "${name} read eeprom register" - id: read_eeprom_register0 - optionsmap: - 0xAA: "Error Counts" - -switch: - - platform: ble_client - ble_client_id: client0 - name: "${name} enable bluetooth connection" - - - platform: jbd_bms_ble - jbd_bms_ble_id: bms0 - charging: - name: "${name} charging" - discharging: - name: "${name} discharging" - -# Uncomment this section if you want to update the error count sensors periodically -# -# interval: -# - interval: 30min -# then: -# - select.set: -# id: read_eeprom_register0 -# option: "Error Counts" \ No newline at end of file diff --git a/KONVER_STRATEGY.md b/KONVER_STRATEGY.md deleted file mode 100644 index 1670c101..00000000 --- a/KONVER_STRATEGY.md +++ /dev/null @@ -1,81 +0,0 @@ -# Konver.ai Integration: Strategie & Architektur - -**Status:** Vertrag unterzeichnet (Fokus: Telefon-Enrichment). -**Risiko:** Wegfall von Dealfront (Lead Gen) ohne adäquaten, automatisierten Ersatz. -**Ziel:** Nutzung von Konver.ai nicht nur als manuelles "Telefonbuch", sondern als **skalierbare Quelle** für die Lead-Fabrik (Company Explorer). - -## 1. Das Zielszenario (The "Golden Flow") - -Wir integrieren Konver.ai via API direkt in den Company Explorer. Der CE fungiert als Gatekeeper, um Credits zu sparen und Dubletten zu verhindern. - -```mermaid -flowchart TD - subgraph "RoboPlanet Ecosystem" - Notion[("Notion Strategy\n(Verticals/Pains)")] - SO[("SuperOffice CRM\n(Bestand)")] - CE["Company Explorer\n(The Brain)"] - end - - subgraph "External Sources" - Konver["Konver.ai API"] - Web["Web / Google / Wiki"] - end - - %% Data Flow - Notion -->|1. Sync Strategy| CE - SO -->|2. Import Existing (Blocklist)| CE - - CE -->|3. Search Query + Exclusion List| Konver - Note right of Konver: "Suche: Altenheime > 10 Mio\nExclude: Domain-Liste aus SO" - - Konver -->|4. Net New Candidates| CE - - CE -->|5. Deep Dive (Robotik-Check)| Web - - CE -->|6. Enrich Contact (Phone/Mail)| Konver - Note right of CE: "Nur für Firmen mit\nhohem Robotik-Score!" - - CE -->|7. Export Qualified Lead| SO -``` - -## 2. Die kritische Lücke: "Exclusion List" - -Da Dealfront (unser bisheriges "Fischnetz") abgeschaltet wird, müssen wir Konver zur **Neukunden-Generierung** nutzen. -Ohne eine **Ausschluss-Liste (Exclusion List)** bei der Suche verbrennen wir Geld und Zeit: - -1. **Kosten:** Wir zahlen Credits für Firmen/Kontakte, die wir schon haben. -2. **Daten-Hygiene:** Wir importieren Dubletten, die wir mühsam bereinigen müssen. -3. **Blindflug:** Wir wissen vor dem Kauf nicht, ob der Datensatz "netto neu" ist. - -### Forderung an Konver (Technisches Onboarding) - -*"Um Konver.ai als strategischen Nachfolger für Dealfront in unserer Marketing-Automation nutzen zu können, benötigen wir zwingend API-Funktionen zur **Deduplizierung VOR dem Datenkauf**."* - -**Konkrete Features:** -* **Domain-Exclusion:** Upload einer Liste (z.B. 5.000 Domains), die in der API-Suche *nicht* zurückgegeben werden. -* **Contact-Check:** Prüfung (z.B. Hash-Abgleich), ob eine E-Mail-Adresse bereits "bekannt" ist, bevor Kontaktdaten enthüllt (und berechnet) werden. - -## 3. Workflow-Varianten - -### A. Der "Smart Enricher" (Wirtschaftlich) -Wir nutzen Konver nur für Firmen, die **wirklich** relevant sind. - -1. **Scraping:** Company Explorer findet 100 Altenheime (Web-Suche). -2. **Filterung:** KI prüft Websites -> 40 davon sind relevant (haben große Flächen). -3. **Enrichment:** Nur für diese 40 fragen wir Konver via API: *"Gib mir den Facility Manager + Handy"*. -4. **Ergebnis:** Wir zahlen 40 Credits statt 100. Hohe Effizienz. - -### B. Der "Mass Loader" (Teuer & Dumm - zu vermeiden) -1. Wir laden "Alle Altenheime" aus Konver direkt nach SuperOffice. -2. Wir zahlen 100 Credits. -3. Der Vertrieb ruft an -> 60 davon sind ungeeignet (zu klein, kein Bedarf). -4. **Ergebnis:** 60 Credits verbrannt, Vertrieb frustriert. - -## 4. Fazit & Next Steps - -Wir müssen im Onboarding-Gespräch klären: -1. **API-Doku:** Wo ist die Dokumentation für `Search` und `Enrich` Endpoints? -2. **Exclusion:** Wie filtern wir Bestandskunden im API-Call? -3. **Bulk-Enrichment:** Können wir Listen (Domains) zum Anreichern hochladen? - -Ohne diese Features ist Konver ein Rückschritt in die manuelle Einzelbearbeitung. diff --git a/MIGRATION_PLAN.md b/MIGRATION_PLAN.md deleted file mode 100644 index a985d8b9..00000000 --- a/MIGRATION_PLAN.md +++ /dev/null @@ -1,161 +0,0 @@ -# Migrations-Plan: Legacy GSheets -> Company Explorer (Robotics Edition v0.8.5) - -**Kontext:** Neuanfang für die Branche **Robotik & Facility Management**. -**Ziel:** Ablösung von Google Sheets/CLI durch eine Web-App ("Company Explorer") mit SQLite-Backend. - -## 1. Strategische Neuausrichtung - -| Bereich | Alt (Legacy) | Neu (Robotics Edition) | -| :--- | :--- | :--- | -| **Daten-Basis** | Google Sheets | **SQLite** (Lokal, performant, filterbar). | -| **Ziel-Daten** | Allgemein / Kundenservice | **Quantifizierbares Potenzial** (z.B. 4500m² Fläche, 120 Betten). | -| **Branchen** | KI-Vorschlag (Freitext) | **Strict Mode:** Mapping auf definierte Notion-Liste (z.B. "Hotellerie", "Automotive"). | -| **Bewertung** | 0-100 Score (Vage) | **Data-Driven:** Rohwert (Scraper/Search) -> Standardisierung (Formel) -> Potenzial. | -| **Analytics** | Techniker-ML-Modell | **Deaktiviert**. Fokus auf harte Fakten. | -| **Operations** | D365 Sync (Broken) | **Excel-Import & Deduplizierung**. Fokus auf Matching externer Listen gegen Bestand. | - -## 2. Architektur & Komponenten-Mapping - -Das System wird in `company-explorer/` neu aufgebaut. Wir lösen Abhängigkeiten zur Root `helpers.py` auf. - -### A. Core Backend (`backend/`) - -| Komponente | Aufgabe & Neue Logik | Prio | -| :--- | :--- | :--- | -| **Database** | Ersetzt `GoogleSheetHandler`. Speichert Firmen & "Enrichment Blobs". | 1 | -| **Importer** | Ersetzt `SyncManager`. Importiert Excel-Dumps (CRM) und Event-Listen. | 1 | -| **Deduplicator** | Ersetzt `company_deduplicator.py`. **Kern-Feature:** Checkt Event-Listen gegen DB. Muss "intelligent" matchen (Name + Ort + Web). | 1 | -| **Scraper (Base)** | Extrahiert Text von Websites. Basis für alle Analysen. | 1 | -| **Classification Service** | **NEU (v0.7.0).** Zweistufige Logik:
1. Strict Industry Classification.
2. Metric Extraction Cascade (Web -> Wiki -> SerpAPI). | 1 | -| **Marketing Engine** | Ersetzt `generate_marketing_text.py`. Nutzt neue `marketing_wissen_robotics.yaml`. | 3 | - -**Identifizierte Hauptdatei:** `company-explorer/backend/app.py` - -### B. Frontend (`frontend/`) - React - -* **View 1: Der "Explorer":** DataGrid aller Firmen. Filterbar nach "Roboter-Potential" und Status. -* **View 2: Der "Inspector":** Detailansicht einer Firma. Zeigt gefundene Signale ("Hat SPA Bereich"). Manuelle Korrektur-Möglichkeit. - * **Identifizierte Komponente:** `company-explorer/frontend/src/components/Inspector.tsx` -* **View 3: "List Matcher":** Upload einer Excel-Liste -> Anzeige von Duplikaten -> Button "Neue importieren". -* **View 4: "Settings":** Konfiguration von Branchen, Rollen und Robotik-Logik. - * **Frontend "Settings" Komponente:** `company-explorer/frontend/src/components/RoboticsSettings.tsx` - -### C. Architekturmuster für die Client-Integration - -Um externen Diensten (wie der `lead-engine`) eine einfache und robuste Anbindung an den `company-explorer` zu ermöglichen, wurde ein standardisiertes Client-Connector-Muster implementiert. - -| Komponente | Aufgabe & Neue Logik | -| :--- | :--- | -| **`company_explorer_connector.py`** | **NEU:** Ein zentrales Python-Skript, das als "offizieller" Client-Wrapper für die API des Company Explorers dient. Es kapselt die Komplexität der asynchronen Enrichment-Prozesse. | -| **`handle_company_workflow()`** | Die Kernfunktion des Connectors. Sie implementiert den vollständigen "Find-or-Create-and-Enrich"-Workflow:
1. **Prüfen:** Stellt fest, ob ein Unternehmen bereits existiert.
2. **Erstellen:** Legt das Unternehmen an, falls es neu ist.
3. **Anstoßen:** Startet den asynchronen `discover`-Prozess.
4. **Warten (Polling):** Überwacht den Status des Unternehmens, bis eine Website gefunden wurde.
5. **Analysieren:** Startet den asynchronen `analyze`-Prozess.
**Vorteil:** Bietet dem aufrufenden Dienst eine einfache, quasi-synchrone Schnittstelle und stellt sicher, dass die Prozessschritte in der korrekten Reihenfolge ausgeführt werden. | - -## 3. Umgang mit Shared Code (`helpers.py` & Co.) - -Wir kapseln das neue Projekt vollständig ab ("Fork & Clean"). - -* **Quelle:** `helpers.py` (Root) -* **Ziel:** `company-explorer/backend/lib/core_utils.py` -* **Aktion:** Wir kopieren nur relevante Teile und ergänzen sie (z.B. `safe_eval_math`, `run_serp_search`). - -## 4. Datenstruktur (SQLite Schema) - -### Tabelle `companies` (Stammdaten & Analyse) -* `id` (PK) -* `name` (String) -* `website` (String) -* `crm_id` (String, nullable - Link zum D365) -* `industry_crm` (String - Die "erlaubte" Branche aus Notion) -* `city` (String) -* `country` (String - Standard: "DE" oder aus Impressum) -* `status` (Enum: NEW, IMPORTED, ENRICHED, QUALIFIED) -* **NEU (v0.7.0):** - * `calculated_metric_name` (String - z.B. "Anzahl Betten") - * `calculated_metric_value` (Float - z.B. 180) - * `calculated_metric_unit` (String - z.B. "Betten") - * `standardized_metric_value` (Float - z.B. 4500) - * `standardized_metric_unit` (String - z.B. "m²") - * `metric_source` (String - "website", "wikipedia", "serpapi") - -### Tabelle `signals` (Deprecated) -* *Veraltet ab v0.7.0. Wird durch quantitative Metriken in `companies` ersetzt.* - -### Tabelle `contacts` (Ansprechpartner) -* `id` (PK) -* `account_id` (FK -> companies.id) -* `gender`, `title`, `first_name`, `last_name`, `email` -* `job_title` (Visitenkarte) -* `role` (Standardisierte Rolle: "Operativer Entscheider", etc.) -* `status` (Marketing Status) - -### Tabelle `industries` (Branchen-Fokus - Synced from Notion) -* `id` (PK) -* `notion_id` (String, Unique) -* `name` (String - "Vertical" in Notion) -* `description` (Text - "Definition" in Notion) -* `metric_type` (String - "Metric Type") -* `min_requirement` (Float - "Min. Requirement") -* `whale_threshold` (Float - "Whale Threshold") -* `proxy_factor` (Float - "Proxy Factor") -* `scraper_search_term` (String - "Scraper Search Term") -* `scraper_keywords` (Text - "Scraper Keywords") -* `standardization_logic` (String - "Standardization Logic") - -### Tabelle `job_role_mappings` (Rollen-Logik) -* `id` (PK) -* `pattern` (String - Regex für Jobtitles) -* `role` (String - Zielrolle) - -## 7. Historie & Fixes (Jan 2026) - - * **[CRITICAL] v0.7.4: Service Restoration & Logic Fix (Jan 24, 2026)** - * **[STABILITY] v0.7.3: Hardening Metric Parser & Regression Testing (Jan 23, 2026)** - * **[STABILITY] v0.7.2: Robust Metric Parsing (Jan 23, 2026)** - * **[STABILITY] v0.7.1: AI Robustness & UI Fixes (Jan 21, 2026)** - * **[MAJOR] v0.7.0: Quantitative Potential Analysis (Jan 20, 2026)** - * **[UPGRADE] v0.6.x: Notion Integration & UI Improvements** - -## 14. Upgrade v2.0 (Feb 18, 2026): "Lead-Fabrik" Erweiterung - -Dieses Upgrade transformiert den Company Explorer in das zentrale Gehirn der Lead-Generierung (Vorratskammer). - -### 14.1 Detaillierte Logik der neuen Datenfelder - -Um Gemini CLI (dem Bautrupp) die Umsetzung zu ermöglichen, hier die semantische Bedeutung der neuen Spalten: - -#### Tabelle `companies` (Qualitäts- & Abgleich-Metriken) - -* **`confidence_score` (FLOAT, 0.0 - 1.0):** Indikator für die Sicherheit der KI-Klassifizierung. `> 0.8` = Grün. -* **`data_mismatch_score` (FLOAT, 0.0 - 1.0):** Abweichung zwischen CRM-Bestand und Web-Recherche (z.B. Umzug). -* **`crm_name`, `crm_address`, `crm_website`, `crm_vat`:** Read-Only Snapshot aus SuperOffice zum Vergleich. -* **Status-Flags:** `website_scrape_status` und `wiki_search_status`. - -#### Tabelle `industries` (Strategie-Parameter) - -* **`pains` / `gains`:** Strukturierte Textblöcke (getrennt durch `[Primary Product]` und `[Secondary Product]`). -* **`ops_focus_secondary` (BOOLEAN):** Steuerung für rollenspezifische Produkt-Priorisierung. - ---- - -## 15. Offene Arbeitspakete (Bauleitung) - -Anweisungen für den "Bautrupp" (Gemini CLI). - -### Task 1: UI-Anpassung - Side-by-Side CRM View & Settings -(In Arbeit / Teilweise erledigt durch Gemini CLI) - -### Task 2: Intelligenter CRM-Importer (Bestandsdaten) - -**Ziel:** Importieren der `demo_100.xlsx` in die SQLite-Datenbank. - -**Anforderungen:** -1. **PLZ-Handling:** Zwingend als **String** einlesen (führende Nullen erhalten). -2. **Normalisierung:** Website bereinigen (kein `www.`, `https://`). -3. **Matching:** Kaskade über CRM-ID, VAT, Domain, Fuzzy Name. -4. **Isolierung:** Nur `crm_` Spalten updaten, Golden Records unberührt lassen. - ---- - -## 16. Deployment-Referenz (NAS) -* **Pfad:** `/volume1/homes/Floke/python/brancheneinstufung/company-explorer` -* **DB:** `/app/companies_v3_fixed_2.db` -* **Sync:** `docker exec -it company-explorer python backend/scripts/sync_notion_to_ce_enhanced.py` diff --git a/b2b-marketing-assistant/Dockerfile b/b2b-marketing-assistant/Dockerfile index 59502482..5200f649 100644 --- a/b2b-marketing-assistant/Dockerfile +++ b/b2b-marketing-assistant/Dockerfile @@ -45,8 +45,6 @@ COPY --from=frontend-builder /app/dist ./dist # Copy the main Python orchestrator script from the project root COPY b2b_marketing_orchestrator.py . -# Copy Gemini API Key file if it exists in root -COPY gemini_api_key.txt . # Expose the port the Node.js server will run on EXPOSE 3002 diff --git a/b2b_marketing_orchestrator.py b/b2b-marketing-assistant/b2b_marketing_orchestrator.py similarity index 99% rename from b2b_marketing_orchestrator.py rename to b2b-marketing-assistant/b2b_marketing_orchestrator.py index 450532b1..653c4eef 100644 --- a/b2b_marketing_orchestrator.py +++ b/b2b-marketing-assistant/b2b_marketing_orchestrator.py @@ -333,12 +333,11 @@ PROMPTS = { # --- API & SCRAPING HELPERS --- def load_api_key(): - try: - with open("gemini_api_key.txt", "r") as f: - return f.read().strip() - except FileNotFoundError: - logging.error("API key file 'gemini_api_key.txt' not found.") + api_key = os.getenv("GEMINI_API_KEY") + if not api_key: + logging.error("GEMINI_API_KEY environment variable not found.") return None + return api_key def call_gemini_api(prompt, api_key, retries=3): url = f"https://generativelanguage.googleapis.com/v1/models/gemini-2.5-flash:generateContent?key={api_key}" diff --git a/alt_roboplanet-gtm-strategy-2026-01-14.md b/b2b-marketing-assistant/docs/alt_roboplanet-gtm-strategy-2026-01-14.md similarity index 100% rename from alt_roboplanet-gtm-strategy-2026-01-14.md rename to b2b-marketing-assistant/docs/alt_roboplanet-gtm-strategy-2026-01-14.md diff --git a/b2b-marketing-analysis_Roboplanet-2.md b/b2b-marketing-assistant/docs/b2b-marketing-analysis_Roboplanet-2.md similarity index 100% rename from b2b-marketing-analysis_Roboplanet-2.md rename to b2b-marketing-assistant/docs/b2b-marketing-analysis_Roboplanet-2.md diff --git a/b2b-marketing-analysis_Roboplanet.md b/b2b-marketing-assistant/docs/b2b-marketing-analysis_Roboplanet.md similarity index 100% rename from b2b-marketing-analysis_Roboplanet.md rename to b2b-marketing-assistant/docs/b2b-marketing-analysis_Roboplanet.md diff --git a/neu_roboplanet-gtm-strategy-2026-01-14.md b/b2b-marketing-assistant/docs/neu_roboplanet-gtm-strategy-2026-01-14.md similarity index 100% rename from neu_roboplanet-gtm-strategy-2026-01-14.md rename to b2b-marketing-assistant/docs/neu_roboplanet-gtm-strategy-2026-01-14.md diff --git a/roboplanet-gtm-strategy-2026-01-03 (1).md b/b2b-marketing-assistant/docs/roboplanet-gtm-strategy-2026-01-03 (1).md similarity index 100% rename from roboplanet-gtm-strategy-2026-01-03 (1).md rename to b2b-marketing-assistant/docs/roboplanet-gtm-strategy-2026-01-03 (1).md diff --git a/roboplanet-gtm-strategy-2026-01-03 (2).md b/b2b-marketing-assistant/docs/roboplanet-gtm-strategy-2026-01-03 (2).md similarity index 100% rename from roboplanet-gtm-strategy-2026-01-03 (2).md rename to b2b-marketing-assistant/docs/roboplanet-gtm-strategy-2026-01-03 (2).md diff --git a/roboplanet-gtm-strategy-2026-01-03.md b/b2b-marketing-assistant/docs/roboplanet-gtm-strategy-2026-01-03.md similarity index 100% rename from roboplanet-gtm-strategy-2026-01-03.md rename to b2b-marketing-assistant/docs/roboplanet-gtm-strategy-2026-01-03.md diff --git a/roboplanet-gtm-strategy-v3.md b/b2b-marketing-assistant/docs/roboplanet-gtm-strategy-v3.md similarity index 100% rename from roboplanet-gtm-strategy-v3.md rename to b2b-marketing-assistant/docs/roboplanet-gtm-strategy-v3.md diff --git a/roboplanet_b2b_analyse.md b/b2b-marketing-assistant/docs/roboplanet_b2b_analyse.md similarity index 100% rename from roboplanet_b2b_analyse.md rename to b2b-marketing-assistant/docs/roboplanet_b2b_analyse.md diff --git a/v3_roboplanet-gtm-strategy-2026-01-20.md b/b2b-marketing-assistant/docs/v3_roboplanet-gtm-strategy-2026-01-20.md similarity index 100% rename from v3_roboplanet-gtm-strategy-2026-01-20.md rename to b2b-marketing-assistant/docs/v3_roboplanet-gtm-strategy-2026-01-20.md diff --git a/v4_roboplanet-gtm-strategy-2026-01-20.md b/b2b-marketing-assistant/docs/v4_roboplanet-gtm-strategy-2026-01-20.md similarity index 100% rename from v4_roboplanet-gtm-strategy-2026-01-20.md rename to b2b-marketing-assistant/docs/v4_roboplanet-gtm-strategy-2026-01-20.md diff --git a/check_settings_api.py b/check_settings_api.py new file mode 100644 index 00000000..c6ac6fbd --- /dev/null +++ b/check_settings_api.py @@ -0,0 +1,55 @@ +import requests +import os + +# --- Configuration --- +def load_env_manual(path): + if not os.path.exists(path): + print(f"⚠️ Warning: .env file not found at {path}") + return + with open(path) as f: + for line in f: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + key, val = line.split('=', 1) + os.environ.setdefault(key.strip(), val.strip()) + +load_env_manual('/app/.env') + +API_USER = os.getenv("API_USER") +API_PASS = os.getenv("API_PASSWORD") +CE_URL = "http://127.0.0.1:8000" + +endpoints_to_check = { + "Industries": "/api/industries", + "Robotics Categories": "/api/robotics/categories", + "Job Roles": "/api/job_roles" +} + +def check_settings_endpoints(): + print("="*60) + print("🩺 Running Settings Endpoints Health Check...") + print("="*60) + + all_ok = True + for name, endpoint in endpoints_to_check.items(): + url = f"{CE_URL}{endpoint}" + print(f"--- Checking {name} ({url}) ---") + try: + response = requests.get(url, auth=(API_USER, API_PASS), timeout=5) + if response.status_code == 200: + print(f" ✅ SUCCESS: Received {len(response.json())} items.") + else: + print(f" ❌ FAILURE: Status {response.status_code}, Response: {response.text}") + all_ok = False + except requests.exceptions.RequestException as e: + print(f" ❌ FATAL: Connection error: {e}") + all_ok = False + + return all_ok + +if __name__ == "__main__": + if check_settings_endpoints(): + print("\n✅ All settings endpoints are healthy.") + else: + print("\n🔥 One or more settings endpoints failed.") + exit(1) diff --git a/check_tables.py b/check_tables.py deleted file mode 100644 index 127548ce..00000000 --- a/check_tables.py +++ /dev/null @@ -1,21 +0,0 @@ -import sqlite3 - -db_path = "/app/company-explorer/companies_v3_fixed_2.db" -conn = sqlite3.connect(db_path) -cursor = conn.cursor() - -cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") -tables = cursor.fetchall() -print(f"Tables in {db_path}: {tables}") - -# Check content of 'signals' if it exists -if ('signals',) in tables: - print("\nChecking 'signals' table for Wolfra (id=12)...") - cursor.execute("SELECT * FROM signals WHERE account_id=12") - columns = [desc[0] for desc in cursor.description] - rows = cursor.fetchall() - for row in rows: - print(dict(zip(columns, row))) - -conn.close() - diff --git a/company-explorer/Dockerfile b/company-explorer/Dockerfile index 27105618..b569d98a 100644 --- a/company-explorer/Dockerfile +++ b/company-explorer/Dockerfile @@ -3,13 +3,20 @@ # It creates a dedicated 'frontend' directory inside the container to avoid potential # file conflicts in the root directory. FROM node:20-slim AS frontend-builder -WORKDIR /app -# Copy the entire frontend project into a 'frontend' subdirectory -COPY frontend ./frontend -# Set the working directory to the new subdirectory WORKDIR /app/frontend -# Install dependencies and build the project from within its own directory -RUN npm install --no-audit --no-fund + +# 1. Copy ONLY package files first (better caching, no host node_modules contamination) +COPY frontend/package.json ./ + +# 2. Clean install dependencies +# We ignore the local package-lock.json to force a resolution compatible with the container environment +RUN npm install --no-audit --no-fund && \ + npx update-browserslist-db@latest + +# 3. Copy source code AFTER install +COPY frontend/ ./ + +# 4. Build RUN npm run build # --- STAGE 2: Backend Builder --- diff --git a/company-explorer/MIGRATION_PLAN.md b/company-explorer/MIGRATION_PLAN.md deleted file mode 100644 index 69f554c9..00000000 --- a/company-explorer/MIGRATION_PLAN.md +++ /dev/null @@ -1,178 +0,0 @@ -# Migrations-Plan: Legacy GSheets -> Company Explorer (Robotics Edition v0.8.5) - -**Kontext:** Neuanfang für die Branche **Robotik & Facility Management**. -**Ziel:** Ablösung von Google Sheets/CLI durch eine Web-App ("Company Explorer") mit SQLite-Backend. - -## 1. Strategische Neuausrichtung - -| Bereich | Alt (Legacy) | Neu (Robotics Edition) | -| :--- | :--- | :--- | -| **Daten-Basis** | Google Sheets | **SQLite** (Lokal, performant, filterbar). | -| **Ziel-Daten** | Allgemein / Kundenservice | **Quantifizierbares Potenzial** (z.B. 4500m² Fläche, 120 Betten). | -| **Branchen** | KI-Vorschlag (Freitext) | **Strict Mode:** Mapping auf definierte Notion-Liste (z.B. "Hotellerie", "Automotive"). | -| **Bewertung** | 0-100 Score (Vage) | **Data-Driven:** Rohwert (Scraper/Search) -> Standardisierung (Formel) -> Potenzial. | -| **Analytics** | Techniker-ML-Modell | **Deaktiviert**. Fokus auf harte Fakten. | -| **Operations** | D365 Sync (Broken) | **Excel-Import & Deduplizierung**. Fokus auf Matching externer Listen gegen Bestand. | - -## 2. Architektur & Komponenten-Mapping - -Das System wird in `company-explorer/` neu aufgebaut. Wir lösen Abhängigkeiten zur Root `helpers.py` auf. - -### A. Core Backend (`backend/`) - -| Komponente | Aufgabe & Neue Logik | Prio | -| :--- | :--- | :--- | -| **Database** | Ersetzt `GoogleSheetHandler`. Speichert Firmen & "Enrichment Blobs". | 1 | -| **Importer** | Ersetzt `SyncManager`. Importiert Excel-Dumps (CRM) und Event-Listen. | 1 | -| **Deduplicator** | Ersetzt `company_deduplicator.py`. **Kern-Feature:** Checkt Event-Listen gegen DB. Muss "intelligent" matchen (Name + Ort + Web). | 1 | -| **Scraper (Base)** | Extrahiert Text von Websites. Basis für alle Analysen. | 1 | -| **Classification Service** | **NEU (v0.7.0).** Zweistufige Logik:
1. Strict Industry Classification.
2. Metric Extraction Cascade (Web -> Wiki -> SerpAPI). | 1 | -| **Marketing Engine** | Ersetzt `generate_marketing_text.py`. Nutzt neue `marketing_wissen_robotics.yaml`. | 3 | - -**Identifizierte Hauptdatei:** `company-explorer/backend/app.py` - -### B. Frontend (`frontend/`) - React - -* **View 1: Der "Explorer":** DataGrid aller Firmen. Filterbar nach "Roboter-Potential" und Status. -* **View 2: Der "Inspector":** Detailansicht einer Firma. Zeigt gefundene Signale ("Hat SPA Bereich"). Manuelle Korrektur-Möglichkeit. - * **Identifizierte Komponente:** `company-explorer/frontend/src/components/Inspector.tsx` -* **View 3: "List Matcher":** Upload einer Excel-Liste -> Anzeige von Duplikaten -> Button "Neue importieren". -* **View 4: "Settings":** Konfiguration von Branchen, Rollen und Robotik-Logik. - * **Frontend "Settings" Komponente:** `company-explorer/frontend/src/components/RoboticsSettings.tsx` - -### C. Architekturmuster für die Client-Integration - -Um externen Diensten (wie der `lead-engine`) eine einfache und robuste Anbindung an den `company-explorer` zu ermöglichen, wurde ein standardisiertes Client-Connector-Muster implementiert. - -| Komponente | Aufgabe & Neue Logik | -| :--- | :--- | -| **`company_explorer_connector.py`** | **NEU:** Ein zentrales Python-Skript, das als "offizieller" Client-Wrapper für die API des Company Explorers dient. Es kapselt die Komplexität der asynchronen Enrichment-Prozesse. | -| **`handle_company_workflow()`** | Die Kernfunktion des Connectors. Sie implementiert den vollständigen "Find-or-Create-and-Enrich"-Workflow:
1. **Prüfen:** Stellt fest, ob ein Unternehmen bereits existiert.
2. **Erstellen:** Legt das Unternehmen an, falls es neu ist.
3. **Anstoßen:** Startet den asynchronen `discover`-Prozess.
4. **Warten (Polling):** Überwacht den Status des Unternehmens, bis eine Website gefunden wurde.
5. **Analysieren:** Startet den asynchronen `analyze`-Prozess.
**Vorteil:** Bietet dem aufrufenden Dienst eine einfache, quasi-synchrone Schnittstelle und stellt sicher, dass die Prozessschritte in der korrekten Reihenfolge ausgeführt werden. | - -### D. Provisioning API (Internal) - -Für die nahtlose Integration mit dem SuperOffice Connector wurde ein dedizierter Endpunkt geschaffen: - -| Endpunkt | Methode | Zweck | -| :--- | :--- | :--- | -| `/api/provision/superoffice-contact` | POST | Liefert "Enrichment-Pakete" (Texte, Status) für einen gegebenen CRM-Kontakt. Greift auf `MarketingMatrix` zu. | - -## 3. Umgang mit Shared Code (`helpers.py` & Co.) - -Wir kapseln das neue Projekt vollständig ab ("Fork & Clean"). - -* **Quelle:** `helpers.py` (Root) -* **Ziel:** `company-explorer/backend/lib/core_utils.py` -* **Aktion:** Wir kopieren nur relevante Teile und ergänzen sie (z.B. `safe_eval_math`, `run_serp_search`). - -## 4. Datenstruktur (SQLite Schema) - -### Tabelle `companies` (Stammdaten & Analyse) -* `id` (PK) -* `name` (String) -* `website` (String) -* `crm_id` (String, nullable - Link zum D365) -* `industry_crm` (String - Die "erlaubte" Branche aus Notion) -* `city` (String) -* `country` (String - Standard: "DE" oder aus Impressum) -* `status` (Enum: NEW, IMPORTED, ENRICHED, QUALIFIED) -* **NEU (v0.7.0):** - * `calculated_metric_name` (String - z.B. "Anzahl Betten") - * `calculated_metric_value` (Float - z.B. 180) - * `calculated_metric_unit` (String - z.B. "Betten") - * `standardized_metric_value` (Float - z.B. 4500) - * `standardized_metric_unit` (String - z.B. "m²") - * `metric_source` (String - "website", "wikipedia", "serpapi") - -### Tabelle `signals` (Deprecated) -* *Veraltet ab v0.7.0. Wird durch quantitative Metriken in `companies` ersetzt.* - -### Tabelle `contacts` (Ansprechpartner) -* `id` (PK) -* `account_id` (FK -> companies.id) -* `gender`, `title`, `first_name`, `last_name`, `email` -* `job_title` (Visitenkarte) -* `role` (Standardisierte Rolle: "Operativer Entscheider", etc.) -* `status` (Marketing Status) - -### Tabelle `industries` (Branchen-Fokus - Synced from Notion) -* `id` (PK) -* `notion_id` (String, Unique) -* `name` (String - "Vertical" in Notion) -* `description` (Text - "Definition" in Notion) -* `metric_type` (String - "Metric Type") -* `min_requirement` (Float - "Min. Requirement") -* `whale_threshold` (Float - "Whale Threshold") -* `proxy_factor` (Float - "Proxy Factor") -* `scraper_search_term` (String - "Scraper Search Term") -* `scraper_keywords` (Text - "Scraper Keywords") -* `standardization_logic` (String - "Standardization Logic") - -### Tabelle `job_role_mappings` (Rollen-Logik) -* `id` (PK) -* `pattern` (String - Regex für Jobtitles) -* `role` (String - Zielrolle) - -### Tabelle `marketing_matrix` (NEU v2.1) -* **Zweck:** Speichert statische, genehmigte Marketing-Texte (Notion Sync). -* `id` (PK) -* `industry_id` (FK -> industries.id) -* `role_id` (FK -> job_role_mappings.id) -* `subject` (Text) -* `intro` (Text) -* `social_proof` (Text) - -## 7. Historie & Fixes (Jan 2026) - - * **[CRITICAL] v0.7.4: Service Restoration & Logic Fix (Jan 24, 2026)** - * **[STABILITY] v0.7.3: Hardening Metric Parser & Regression Testing (Jan 23, 2026)** - * **[STABILITY] v0.7.2: Robust Metric Parsing (Jan 23, 2026)** - * **[STABILITY] v0.7.1: AI Robustness & UI Fixes (Jan 21, 2026)** - * **[MAJOR] v0.7.0: Quantitative Potential Analysis (Jan 20, 2026)** - * **[UPGRADE] v0.6.x: Notion Integration & UI Improvements** - -## 14. Upgrade v2.0 (Feb 18, 2026): "Lead-Fabrik" Erweiterung - -Dieses Upgrade transformiert den Company Explorer in das zentrale Gehirn der Lead-Generierung (Vorratskammer). - -### 14.1 Detaillierte Logik der neuen Datenfelder - -Um Gemini CLI (dem Bautrupp) die Umsetzung zu ermöglichen, hier die semantische Bedeutung der neuen Spalten: - -#### Tabelle `companies` (Qualitäts- & Abgleich-Metriken) - -* **`confidence_score` (FLOAT, 0.0 - 1.0):** Indikator für die Sicherheit der KI-Klassifizierung. `> 0.8` = Grün. -* **`data_mismatch_score` (FLOAT, 0.0 - 1.0):** Abweichung zwischen CRM-Bestand und Web-Recherche (z.B. Umzug). -* **`crm_name`, `crm_address`, `crm_website`, `crm_vat`:** Read-Only Snapshot aus SuperOffice zum Vergleich. -* **Status-Flags:** `website_scrape_status` und `wiki_search_status`. - -#### Tabelle `industries` (Strategie-Parameter) - -* **`pains` / `gains`:** Strukturierte Textblöcke (getrennt durch `[Primary Product]` und `[Secondary Product]`). -* **`ops_focus_secondary` (BOOLEAN):** Steuerung für rollenspezifische Produkt-Priorisierung. - ---- - -## 15. Offene Arbeitspakete (Bauleitung) - -Anweisungen für den "Bautrupp" (Gemini CLI). - -### Task 1: UI-Anpassung - Side-by-Side CRM View & Settings -(In Arbeit / Teilweise erledigt durch Gemini CLI) - -### Task 2: Intelligenter CRM-Importer (Bestandsdaten) - -**Ziel:** Importieren der `demo_100.xlsx` in die SQLite-Datenbank. - -**Anforderungen:** -1. **PLZ-Handling:** Zwingend als **String** einlesen (führende Nullen erhalten). -2. **Normalisierung:** Website bereinigen (kein `www.`, `https://`). -3. **Matching:** Kaskade über CRM-ID, VAT, Domain, Fuzzy Name. -4. **Isolierung:** Nur `crm_` Spalten updaten, Golden Records unberührt lassen. - ---- - -## 16. Deployment-Referenz (NAS) -* **Pfad:** `/volume1/homes/Floke/python/brancheneinstufung/company-explorer` -* **DB:** `/app/companies_v3_fixed_2.db` -* **Sync:** `docker exec -it company-explorer python backend/scripts/sync_notion_to_ce_enhanced.py` diff --git a/company-explorer/__init__.py b/company-explorer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/company-explorer/backend/app.py b/company-explorer/backend/app.py index 4c27d82b..c9c99644 100644 --- a/company-explorer/backend/app.py +++ b/company-explorer/backend/app.py @@ -1,4 +1,4 @@ -from fastapi import FastAPI, Depends, HTTPException, Query, BackgroundTasks +from fastapi import FastAPI, Depends, HTTPException, Query, BackgroundTasks, UploadFile, File from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse @@ -8,6 +8,10 @@ from pydantic import BaseModel from datetime import datetime import os import sys +import uuid +import shutil +import re +from collections import Counter from fastapi.security import HTTPBasic, HTTPBasicCredentials import secrets @@ -32,11 +36,13 @@ setup_logging() import logging logger = logging.getLogger(__name__) -from .database import init_db, get_db, Company, Signal, EnrichmentData, RoboticsCategory, Contact, Industry, JobRoleMapping, ReportedMistake, MarketingMatrix +from .database import init_db, get_db, Company, Signal, EnrichmentData, RoboticsCategory, Contact, Industry, JobRolePattern, ReportedMistake, MarketingMatrix, Persona, RawJobTitle from .services.deduplication import Deduplicator from .services.discovery import DiscoveryService from .services.scraping import ScraperService from .services.classification import ClassificationService +from .services.role_mapping import RoleMappingService +from .services.optimization import PatternOptimizationService # Initialize App app = FastAPI( @@ -58,6 +64,14 @@ scraper = ScraperService() classifier = ClassificationService() # Now works without args discovery = DiscoveryService() +# Global State for Long-Running Optimization Task +optimization_status = { + "state": "idle", # idle, processing, completed, error + "progress": 0, + "result": None, + "error": None +} + # --- Pydantic Models --- class CompanyCreate(BaseModel): name: str @@ -66,6 +80,15 @@ class CompanyCreate(BaseModel): website: Optional[str] = None crm_id: Optional[str] = None +class ContactCreate(BaseModel): + company_id: int + first_name: Optional[str] = None + last_name: Optional[str] = None + email: Optional[str] = None + job_title: Optional[str] = None + role: Optional[str] = None + is_primary: bool = True + class BulkImportRequest(BaseModel): names: List[str] @@ -89,6 +112,10 @@ class ProvisioningRequest(BaseModel): so_person_id: Optional[int] = None crm_name: Optional[str] = None crm_website: Optional[str] = None + job_title: Optional[str] = None + crm_industry_name: Optional[str] = None + campaign_tag: Optional[str] = None # NEW: e.g. "messe_2026" + class ProvisioningResponse(BaseModel): status: str @@ -96,8 +123,107 @@ class ProvisioningResponse(BaseModel): website: Optional[str] = None vertical_name: Optional[str] = None role_name: Optional[str] = None + opener: Optional[str] = None # Primary opener (Infrastructure/Cleaning) + opener_secondary: Optional[str] = None # Secondary opener (Service/Logistics) + summary: Optional[str] = None # NEW: AI Research Dossier texts: Dict[str, Optional[str]] = {} + unsubscribe_link: Optional[str] = None + + # Enrichment Data for Write-Back + address_city: Optional[str] = None + address_zip: Optional[str] = None + address_street: Optional[str] = None + address_country: Optional[str] = None + vat_id: Optional[str] = None + +class IndustryDetails(BaseModel): + pains: Optional[str] = None + gains: Optional[str] = None + priority: Optional[str] = None + notes: Optional[str] = None + ops_focus_secondary: bool = False + + class Config: + from_attributes = True + +class MarketingMatrixUpdate(BaseModel): + subject: Optional[str] = None + intro: Optional[str] = None + social_proof: Optional[str] = None + +class MarketingMatrixResponse(BaseModel): + id: int + industry_id: int + persona_id: int + campaign_tag: str + industry_name: str + persona_name: str + subject: Optional[str] = None + intro: Optional[str] = None + social_proof: Optional[str] = None + updated_at: datetime + + + class Config: + from_attributes = True + +class ContactResponse(BaseModel): + id: int + first_name: Optional[str] = None + last_name: Optional[str] = None + job_title: Optional[str] = None + role: Optional[str] = None + email: Optional[str] = None + is_primary: bool + + class Config: + from_attributes = True + +class EnrichmentDataResponse(BaseModel): + id: int + source_type: str + content: Dict[str, Any] + is_locked: bool + wiki_verified_empty: bool + updated_at: datetime + + class Config: + from_attributes = True + +class CompanyDetailsResponse(BaseModel): + id: int + name: str + website: Optional[str] = None + city: Optional[str] = None + country: Optional[str] = None + industry_ai: Optional[str] = None + status: str + + # Metrics + calculated_metric_name: Optional[str] = None + calculated_metric_value: Optional[float] = None + calculated_metric_unit: Optional[str] = None + standardized_metric_value: Optional[float] = None + standardized_metric_unit: Optional[str] = None + metric_source: Optional[str] = None + metric_proof_text: Optional[str] = None + metric_source_url: Optional[str] = None + metric_confidence: Optional[float] = None + + # Openers + ai_opener: Optional[str] = None + ai_opener_secondary: Optional[str] = None + research_dossier: Optional[str] = None + + # Relations + industry_details: Optional[IndustryDetails] = None + contacts: List[ContactResponse] = [] + enrichment_data: List[EnrichmentDataResponse] = [] + + class Config: + from_attributes = True + # --- Events --- @app.on_event("startup") def on_startup(): @@ -108,7 +234,69 @@ def on_startup(): except Exception as e: logger.critical(f"Database init failed: {e}", exc_info=True) -# --- Routes --- +# --- Public Routes (No Auth) --- + +from fastapi.responses import HTMLResponse + +@app.get("/unsubscribe/{token}", response_class=HTMLResponse) +def unsubscribe_contact(token: str, db: Session = Depends(get_db)): + contact = db.query(Contact).filter(Contact.unsubscribe_token == token).first() + + success_html = """ + + + + + Abmeldung erfolgreich + + + +

Sie wurden erfolgreich abgemeldet.

+

Sie werden keine weiteren Marketing-E-Mails von uns erhalten.

+ + + """ + + error_html = """ + + + + + Fehler bei der Abmeldung + + + +

Abmeldung fehlgeschlagen.

+

Der von Ihnen verwendete Link ist ungültig oder abgelaufen. Bitte kontaktieren Sie uns bei Problemen direkt.

+ + + """ + + if not contact: + logger.warning(f"Unsubscribe attempt with invalid token: {token}") + return HTMLResponse(content=error_html, status_code=404) + + if contact.status == "unsubscribed": + logger.info(f"Contact {contact.id} already unsubscribed, showing success page anyway.") + return HTMLResponse(content=success_html, status_code=200) + + contact.status = "unsubscribed" + contact.updated_at = datetime.utcnow() + db.commit() + + logger.info(f"Contact {contact.id} ({contact.email}) unsubscribed successfully via token.") + # Here you would trigger the sync back to SuperOffice in a background task + # background_tasks.add_task(sync_unsubscribe_to_superoffice, contact.id) + + return HTMLResponse(content=success_html, status_code=200) + +# --- API Routes --- @app.get("/api/health") def health_check(username: str = Depends(authenticate_user)): @@ -167,53 +355,97 @@ def provision_superoffice_contact( # 1c. Update CRM Snapshot Data (The Double Truth) changed = False - if req.crm_name: - company.crm_name = req.crm_name - changed = True - if req.crm_website: - company.crm_website = req.crm_website - changed = True + name_changed_significantly = False - # Simple Mismatch Check - if company.website and company.crm_website: - def norm(u): return str(u).lower().replace("https://", "").replace("http://", "").replace("www.", "").strip("/") - if norm(company.website) != norm(company.crm_website): - company.data_mismatch_score = 0.8 # High mismatch + if req.crm_name and req.crm_name != company.crm_name: + logger.info(f"CRM Name Change detected for ID {company.crm_id}: {company.crm_name} -> {req.crm_name}") + company.crm_name = req.crm_name + # If the name changes, we should potentially re-evaluate the whole company + # especially if the status was already ENRICHED + if company.status == "ENRICHED": + name_changed_significantly = True + changed = True + + if req.crm_website: + if company.crm_website != req.crm_website: + company.crm_website = req.crm_website changed = True - else: - if company.data_mismatch_score != 0.0: - company.data_mismatch_score = 0.0 - changed = True + + # ... if changed: company.updated_at = datetime.utcnow() + if name_changed_significantly: + logger.info(f"Triggering FRESH discovery for {company.name} due to CRM name change.") + company.status = "NEW" + # We don't change the internal 'name' yet, Discovery will do that or we keep it as anchor. + # But we must clear old results to avoid stale data. + company.industry_ai = None + company.ai_opener = None + company.ai_opener_secondary = None + background_tasks.add_task(run_discovery_task, company.id) + db.commit() + # If we just triggered a fresh discovery, tell the worker to wait. + if name_changed_significantly: + return ProvisioningResponse( + status="processing", + company_name=company.crm_name + ) + # 2. Find Contact (Person) if req.so_person_id is None: - # Just a company sync, no texts needed + # Just a company sync, but return all company-level metadata return ProvisioningResponse( status="success", company_name=company.name, website=company.website, - vertical_name=company.industry_ai + vertical_name=company.industry_ai, + opener=company.ai_opener, + opener_secondary=company.ai_opener_secondary, + address_city=company.city, + address_street=company.street, + address_zip=company.zip_code, + address_country=company.country, + vat_id=company.crm_vat ) person = db.query(Contact).filter(Contact.so_person_id == req.so_person_id).first() + # Auto-Create/Update Person + if not person: + person = Contact( + company_id=company.id, + so_contact_id=req.so_contact_id, + so_person_id=req.so_person_id, + status="ACTIVE", + unsubscribe_token=str(uuid.uuid4()) + ) + db.add(person) + logger.info(f"Created new person {req.so_person_id} for company {company.name}") + + # Update Job Title & Role logic + if req.job_title and req.job_title != person.job_title: + person.job_title = req.job_title + + # New, service-based classification + role_mapping_service = RoleMappingService(db) + found_role = role_mapping_service.get_role_for_job_title(req.job_title) + + if found_role != person.role: + logger.info(f"Role Change for {person.so_person_id} via Mapping Service: {person.role} -> {found_role}") + person.role = found_role + + if not found_role: + # If no role was found, we log it for future pattern mining + role_mapping_service.add_or_update_unclassified_title(req.job_title) + + db.commit() + db.refresh(person) + # 3. Determine Role - role_name = None - if person and person.role: - role_name = person.role - elif req.job_title: - # Simple classification fallback - mappings = db.query(JobRoleMapping).all() - for m in mappings: - # Check pattern type (Regex vs Simple) - simplified here - pattern_clean = m.pattern.replace("%", "").lower() - if pattern_clean in req.job_title.lower(): - role_name = m.role - break + role_name = person.role # 4. Determine Vertical (Industry) vertical_name = company.industry_ai @@ -223,22 +455,34 @@ def provision_superoffice_contact( if vertical_name and role_name: industry_obj = db.query(Industry).filter(Industry.name == vertical_name).first() + persona_obj = db.query(Persona).filter(Persona.name == role_name).first() - if industry_obj: - # Find any mapping for this role to query the Matrix - # (Assuming Matrix is linked to *one* canonical mapping for this role string) - role_ids = [m.id for m in db.query(JobRoleMapping).filter(JobRoleMapping.role == role_name).all()] + if industry_obj and persona_obj: + # Try to find a campaign-specific entry first + matrix_entry = db.query(MarketingMatrix).filter( + MarketingMatrix.industry_id == industry_obj.id, + MarketingMatrix.persona_id == persona_obj.id, + MarketingMatrix.campaign_tag == req.campaign_tag + ).first() - if role_ids: + # Fallback to standard if no specific entry is found + if not matrix_entry: matrix_entry = db.query(MarketingMatrix).filter( MarketingMatrix.industry_id == industry_obj.id, - MarketingMatrix.role_id.in_(role_ids) + MarketingMatrix.persona_id == persona_obj.id, + MarketingMatrix.campaign_tag == "standard" ).first() - - if matrix_entry: - texts["subject"] = matrix_entry.subject - texts["intro"] = matrix_entry.intro - texts["social_proof"] = matrix_entry.social_proof + + + if matrix_entry: + texts["subject"] = matrix_entry.subject + texts["intro"] = matrix_entry.intro + texts["social_proof"] = matrix_entry.social_proof + + # 6. Construct Unsubscribe Link + unsubscribe_link = None + if person and person.unsubscribe_token: + unsubscribe_link = f"{settings.APP_BASE_URL.rstrip('/')}/unsubscribe/{person.unsubscribe_token}" return ProvisioningResponse( status="success", @@ -246,7 +490,18 @@ def provision_superoffice_contact( website=company.website, vertical_name=vertical_name, role_name=role_name, - texts=texts + opener=company.ai_opener, + opener_secondary=company.ai_opener_secondary, + summary=company.research_dossier, + texts=texts, + unsubscribe_link=unsubscribe_link, + + address_city=company.city, + address_street=company.street, + address_zip=company.zip_code, + address_country=company.country, + # TODO: Add VAT field to Company model if not present, for now using crm_vat if available + vat_id=company.crm_vat ) @app.get("/api/companies") @@ -303,6 +558,8 @@ def export_companies_csv(db: Session = Depends(get_db), username: str = Depends( from fastapi.responses import StreamingResponse output = io.StringIO() + # Add UTF-8 BOM for Excel + output.write('\ufeff') writer = csv.writer(output) # Header @@ -335,7 +592,7 @@ def export_companies_csv(db: Session = Depends(get_db), username: str = Depends( headers={"Content-Disposition": f"attachment; filename=company_export_{datetime.utcnow().strftime('%Y-%m-%d')}.csv"} ) -@app.get("/api/companies/{company_id}") +@app.get("/api/companies/{company_id}", response_model=CompanyDetailsResponse) def get_company(company_id: int, db: Session = Depends(get_db), username: str = Depends(authenticate_user)): company = db.query(Company).options( joinedload(Company.enrichment_data), @@ -349,28 +606,14 @@ def get_company(company_id: int, db: Session = Depends(get_db), username: str = if company.industry_ai: ind = db.query(Industry).filter(Industry.name == company.industry_ai).first() if ind: - industry_details = { - "pains": ind.pains, - "gains": ind.gains, - "priority": ind.priority, - "notes": ind.notes, - "ops_focus_secondary": ind.ops_focus_secondary - } + industry_details = IndustryDetails.model_validate(ind) - # HACK: Attach to response object (Pydantic would be cleaner, but this works for fast prototyping) - # We convert to dict and append - resp = company.__dict__.copy() - resp["industry_details"] = industry_details - # Handle SQLAlchemy internal state - if "_sa_instance_state" in resp: del resp["_sa_instance_state"] - # Handle relationships manually if needed, or let FastAPI encode the SQLAlchemy model + extra dict - # Better: return a custom dict merging both + # FastAPI will automatically serialize the 'company' ORM object into the + # CompanyDetailsResponse schema. We just need to attach the extra 'industry_details'. + response_data = CompanyDetailsResponse.model_validate(company) + response_data.industry_details = industry_details - # Since we use joinedload, relationships are loaded. - # Let's rely on FastAPI's ability to serialize the object, but we need to inject the extra field. - # The safest way without changing Pydantic schemas everywhere is to return a dict. - - return {**resp, "enrichment_data": company.enrichment_data, "contacts": company.contacts, "signals": company.signals} + return response_data @app.post("/api/companies") def create_company(company: CompanyCreate, db: Session = Depends(get_db), username: str = Depends(authenticate_user)): @@ -409,6 +652,53 @@ def bulk_import_companies(req: BulkImportRequest, background_tasks: BackgroundTa db.commit() return {"status": "success", "imported": imported_count} +@app.post("/api/contacts") +def create_contact_endpoint(contact: ContactCreate, db: Session = Depends(get_db), username: str = Depends(authenticate_user)): + # Check if company exists + company = db.query(Company).filter(Company.id == contact.company_id).first() + if not company: + raise HTTPException(status_code=404, detail="Company not found") + + # Automatic Role Mapping logic + final_role = contact.role + if contact.job_title and not final_role: + role_mapping_service = RoleMappingService(db) + found_role = role_mapping_service.get_role_for_job_title(contact.job_title) + if found_role: + final_role = found_role + else: + # Log unclassified title for future mining + role_mapping_service.add_or_update_unclassified_title(contact.job_title) + + # Check if contact with same email already exists for this company + if contact.email: + existing = db.query(Contact).filter(Contact.company_id == contact.company_id, Contact.email == contact.email).first() + if existing: + # Update existing contact + existing.first_name = contact.first_name + existing.last_name = contact.last_name + existing.job_title = contact.job_title + existing.role = final_role + db.commit() + db.refresh(existing) + return existing + + new_contact = Contact( + company_id=contact.company_id, + first_name=contact.first_name, + last_name=contact.last_name, + email=contact.email, + job_title=contact.job_title, + role=final_role, + is_primary=contact.is_primary, + status="ACTIVE", + unsubscribe_token=str(uuid.uuid4()) + ) + db.add(new_contact) + db.commit() + db.refresh(new_contact) + return new_contact + @app.post("/api/companies/{company_id}/override/wikipedia") def override_wikipedia(company_id: int, url: str, background_tasks: BackgroundTasks, db: Session = Depends(get_db), username: str = Depends(authenticate_user)): company = db.query(Company).filter(Company.id == company_id).first() @@ -455,7 +745,367 @@ def list_industries(db: Session = Depends(get_db), username: str = Depends(authe @app.get("/api/job_roles") def list_job_roles(db: Session = Depends(get_db), username: str = Depends(authenticate_user)): - return db.query(JobRoleMapping).order_by(JobRoleMapping.pattern.asc()).all() + return db.query(JobRolePattern).order_by(JobRolePattern.priority.asc()).all() + +# --- Marketing Matrix Endpoints --- + +@app.get("/api/matrix", response_model=List[MarketingMatrixResponse]) +def get_marketing_matrix( + industry_id: Optional[int] = Query(None), + persona_id: Optional[int] = Query(None), + campaign_tag: Optional[str] = Query(None), + db: Session = Depends(get_db), + username: str = Depends(authenticate_user) +): + query = db.query(MarketingMatrix).options( + joinedload(MarketingMatrix.industry), + joinedload(MarketingMatrix.persona) + ) + + if industry_id: + query = query.filter(MarketingMatrix.industry_id == industry_id) + if persona_id: + query = query.filter(MarketingMatrix.persona_id == persona_id) + if campaign_tag: + query = query.filter(MarketingMatrix.campaign_tag == campaign_tag) + + entries = query.all() + + # Map to response model + return [ + MarketingMatrixResponse( + id=e.id, + industry_id=e.industry_id, + persona_id=e.persona_id, + campaign_tag=e.campaign_tag, + industry_name=e.industry.name if e.industry else "Unknown", + persona_name=e.persona.name if e.persona else "Unknown", + subject=e.subject, + intro=e.intro, + social_proof=e.social_proof, + updated_at=e.updated_at + ) for e in entries + ] + +@app.get("/api/matrix/export") +def export_matrix_csv( + industry_id: Optional[int] = Query(None), + persona_id: Optional[int] = Query(None), + db: Session = Depends(get_db), + username: str = Depends(authenticate_user) +): + """ + Exports a CSV of the marketing matrix, optionally filtered. + """ + import io + import csv + from fastapi.responses import StreamingResponse + + query = db.query(MarketingMatrix).options( + joinedload(MarketingMatrix.industry), + joinedload(MarketingMatrix.persona) + ) + + if industry_id: + query = query.filter(MarketingMatrix.industry_id == industry_id) + if persona_id: + query = query.filter(MarketingMatrix.persona_id == persona_id) + + entries = query.all() + + output = io.StringIO() + # Add UTF-8 BOM for Excel + output.write('\ufeff') + writer = csv.writer(output) + + # Header + writer.writerow([ + "ID", "Industry", "Persona", "Subject", "Intro", "Social Proof", "Last Updated" + ]) + + for e in entries: + writer.writerow([ + e.id, + e.industry.name if e.industry else "Unknown", + e.persona.name if e.persona else "Unknown", + e.subject, + e.intro, + e.social_proof, + e.updated_at.strftime('%Y-%m-%d %H:%M:%S') if e.updated_at else "-" + ]) + + output.seek(0) + + filename = f"marketing_matrix_{datetime.utcnow().strftime('%Y-%m-%d')}.csv" + return StreamingResponse( + output, + media_type="text/csv", + headers={"Content-Disposition": f"attachment; filename={filename}"} + ) + +@app.put("/api/matrix/{entry_id}", response_model=MarketingMatrixResponse) +def update_matrix_entry( + entry_id: int, + data: MarketingMatrixUpdate, + db: Session = Depends(get_db), + username: str = Depends(authenticate_user) +): + entry = db.query(MarketingMatrix).options( + joinedload(MarketingMatrix.industry), + joinedload(MarketingMatrix.persona) + ).filter(MarketingMatrix.id == entry_id).first() + + if not entry: + raise HTTPException(status_code=404, detail="Matrix entry not found") + + if data.subject is not None: + entry.subject = data.subject + if data.intro is not None: + entry.intro = data.intro + if data.social_proof is not None: + entry.social_proof = data.social_proof + + entry.updated_at = datetime.utcnow() + db.commit() + db.refresh(entry) + + return MarketingMatrixResponse( + id=entry.id, + industry_id=entry.industry_id, + persona_id=entry.persona_id, + industry_name=entry.industry.name if entry.industry else "Unknown", + persona_name=entry.persona.name if entry.persona else "Unknown", + subject=entry.subject, + intro=entry.intro, + social_proof=entry.social_proof, + updated_at=entry.updated_at + ) + +@app.get("/api/matrix/personas") +def list_personas(db: Session = Depends(get_db), username: str = Depends(authenticate_user)): + return db.query(Persona).all() + +class JobRolePatternCreate(BaseModel): + pattern_type: str + pattern_value: str + role: str + priority: int = 100 + +class JobRolePatternResponse(BaseModel): + id: int + pattern_type: str + pattern_value: str + role: str + priority: int + is_active: bool + created_by: str + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + +class ClassificationResponse(BaseModel): + status: str + processed: int + new_patterns: int + +class OptimizationProposal(BaseModel): + target_role: str + regex: str + explanation: str + priority: int + covered_pattern_ids: List[int] + covered_titles: List[str] + false_positives: List[str] + +class ApplyOptimizationRequest(BaseModel): + target_role: str + regex: str + priority: int + ids_to_delete: List[int] + +def run_optimization_task(): + global optimization_status + optimization_status["state"] = "processing" + optimization_status["result"] = None + optimization_status["error"] = None + + from .database import SessionLocal + db = SessionLocal() + try: + optimizer = PatternOptimizationService(db) + proposals = optimizer.generate_proposals() + optimization_status["result"] = proposals + optimization_status["state"] = "completed" + except Exception as e: + logger.error(f"Optimization task failed: {e}", exc_info=True) + optimization_status["state"] = "error" + optimization_status["error"] = str(e) + finally: + db.close() + +@app.post("/api/job_roles/optimize-start") +def start_pattern_optimization( + background_tasks: BackgroundTasks, + username: str = Depends(authenticate_user) +): + """ + Starts the optimization analysis in the background. + """ + global optimization_status + if optimization_status["state"] == "processing": + return {"status": "already_running"} + + background_tasks.add_task(run_optimization_task) + return {"status": "started"} + +@app.get("/api/job_roles/optimize-status") +def get_pattern_optimization_status( + username: str = Depends(authenticate_user) +): + """ + Poll this endpoint to get the result of the optimization. + """ + return optimization_status + +@app.post("/api/job_roles/apply-optimization") +def apply_pattern_optimization( + req: ApplyOptimizationRequest, + db: Session = Depends(get_db), + username: str = Depends(authenticate_user) +): + """ + Applies a proposal: Creates the new regex and deletes the obsolete exact patterns. + """ + # 1. Create new Regex Pattern + # Check duplicate first + existing = db.query(JobRolePattern).filter(JobRolePattern.pattern_value == req.regex).first() + if not existing: + new_pattern = JobRolePattern( + pattern_type="regex", + pattern_value=req.regex, + role=req.target_role, + priority=req.priority, + created_by="optimizer" + ) + db.add(new_pattern) + logger.info(f"Optimization: Created new regex {req.regex} for {req.target_role}") + + # 2. Delete covered Exact Patterns + if req.ids_to_delete: + db.query(JobRolePattern).filter(JobRolePattern.id.in_(req.ids_to_delete)).delete(synchronize_session=False) + logger.info(f"Optimization: Deleted {len(req.ids_to_delete)} obsolete patterns.") + + db.commit() + return {"status": "success", "message": f"Created regex and removed {len(req.ids_to_delete)} old patterns."} + +@app.post("/api/job_roles", response_model=JobRolePatternResponse) +def create_job_role( + job_role: JobRolePatternCreate, + db: Session = Depends(get_db), + username: str = Depends(authenticate_user) +): + db_job_role = JobRolePattern( + pattern_type=job_role.pattern_type, + pattern_value=job_role.pattern_value, + role=job_role.role, + priority=job_role.priority, + created_by="user" + ) + db.add(db_job_role) + db.commit() + db.refresh(db_job_role) + return db_job_role + +@app.put("/api/job_roles/{role_id}", response_model=JobRolePatternResponse) +def update_job_role( + role_id: int, + job_role: JobRolePatternCreate, + db: Session = Depends(get_db), + username: str = Depends(authenticate_user) +): + db_job_role = db.query(JobRolePattern).filter(JobRolePattern.id == role_id).first() + if not db_job_role: + raise HTTPException(status_code=404, detail="Job role not found") + + db_job_role.pattern_type = job_role.pattern_type + db_job_role.pattern_value = job_role.pattern_value + db_job_role.role = job_role.role + db_job_role.priority = job_role.priority + db_job_role.updated_at = datetime.utcnow() + db.commit() + db.refresh(db_job_role) + return db_job_role + +@app.delete("/api/job_roles/{role_id}") +def delete_job_role( + role_id: int, + db: Session = Depends(get_db), + username: str = Depends(authenticate_user) +): + db_job_role = db.query(JobRolePattern).filter(JobRolePattern.id == role_id).first() + if not db_job_role: + raise HTTPException(status_code=404, detail="Job role not found") + + db.delete(db_job_role) + db.commit() + return {"status": "deleted"} + +@app.post("/api/job_roles/classify-batch", response_model=ClassificationResponse) +def classify_batch_job_roles( + background_tasks: BackgroundTasks, + username: str = Depends(authenticate_user) +): + """ + Triggers a background task to classify all unmapped job titles from the inbox. + """ + background_tasks.add_task(run_batch_classification_task) + return {"status": "queued", "processed": 0, "new_patterns": 0} + +@app.get("/api/job_roles/raw") +def list_raw_job_titles( + limit: int = 100, + unmapped_only: bool = True, + db: Session = Depends(get_db), + username: str = Depends(authenticate_user) +): + """ + Returns unique raw job titles from CRM imports, prioritized by frequency. + """ + query = db.query(RawJobTitle) + if unmapped_only: + query = query.filter(RawJobTitle.is_mapped == False) + + return query.order_by(RawJobTitle.count.desc()).limit(limit).all() + +@app.get("/api/job_roles/suggestions") +def get_job_role_suggestions(db: Session = Depends(get_db), username: str = Depends(authenticate_user)): + """ + Analyzes existing contacts to suggest regex patterns based on frequent keywords per role. + """ + contacts = db.query(Contact).filter(Contact.role != None, Contact.job_title != None).all() + + role_groups = {} + for c in contacts: + if c.role not in role_groups: + role_groups[c.role] = [] + role_groups[c.role].append(c.job_title) + + suggestions = {} + + for role, titles in role_groups.items(): + all_tokens = [] + for t in titles: + # Simple cleaning: keep alphanum, lower + cleaned = re.sub(r'[^\w\s]', ' ', t).lower() + tokens = [w for w in cleaned.split() if len(w) > 3] # Ignore short words + all_tokens.extend(tokens) + + common = Counter(all_tokens).most_common(10) + suggestions[role] = [{"word": w, "count": c} for w, c in common] + + return suggestions @app.get("/api/mistakes") def list_reported_mistakes( @@ -504,6 +1154,87 @@ def update_reported_mistake_status( logger.info(f"Updated status for mistake {mistake_id} to {mistake.status}") return {"status": "success", "mistake": mistake} +# --- Database Management --- + +@app.get("/api/admin/database/download") +def download_database(username: str = Depends(authenticate_user)): + """ + Downloads the current SQLite database file. + """ + db_path = "/app/companies_v3_fixed_2.db" + if not os.path.exists(db_path): + raise HTTPException(status_code=404, detail="Database file not found") + + filename = f"companies_backup_{datetime.utcnow().strftime('%Y-%m-%d_%H-%M')}.db" + return FileResponse(db_path, media_type="application/octet-stream", filename=filename) + +@app.post("/api/admin/database/upload") +async def upload_database( + file: UploadFile = File(...), + username: str = Depends(authenticate_user) +): + """ + Uploads and replaces the SQLite database file. Creating a backup first. + """ + db_path = "/app/companies_v3_fixed_2.db" + backup_path = f"{db_path}.bak.{datetime.utcnow().strftime('%Y-%m-%d_%H-%M-%S')}" + + try: + # Create Backup + if os.path.exists(db_path): + shutil.copy2(db_path, backup_path) + logger.info(f"Created database backup at {backup_path}") + + # Save new file + with open(db_path, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + logger.info(f"Database replaced via upload by user {username}") + return {"status": "success", "message": "Database uploaded successfully. Please restart the container to apply changes."} + + except Exception as e: + logger.error(f"Database upload failed: {e}", exc_info=True) + # Try to restore backup if something went wrong during write + if os.path.exists(backup_path): + shutil.copy2(backup_path, db_path) + logger.warning("Restored database from backup due to upload failure.") + + raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}") + +# --- Regex Testing --- + +class RegexTestRequest(BaseModel): + pattern: str + pattern_type: str = "regex" # regex, exact, startswith + test_string: str + +@app.post("/api/job_roles/test-pattern") +def test_job_role_pattern(req: RegexTestRequest, username: str = Depends(authenticate_user)): + """ + Tests if a given pattern matches a test string. + """ + try: + is_match = False + normalized_test = req.test_string.lower().strip() + pattern = req.pattern.lower().strip() + + if req.pattern_type == "regex": + if re.search(pattern, normalized_test, re.IGNORECASE): + is_match = True + elif req.pattern_type == "exact": + if pattern == normalized_test: + is_match = True + elif req.pattern_type == "startswith": + if normalized_test.startswith(pattern): + is_match = True + + return {"match": is_match} + except re.error as e: + return {"match": False, "error": f"Invalid Regex: {str(e)}"} + except Exception as e: + logger.error(f"Pattern test error: {e}") + return {"match": False, "error": str(e)} + @app.post("/api/enrich/discover") def discover_company(req: AnalysisRequest, background_tasks: BackgroundTasks, db: Session = Depends(get_db), username: str = Depends(authenticate_user)): company = db.query(Company).filter(Company.id == req.company_id).first() @@ -780,42 +1511,108 @@ def run_analysis_task(company_id: int): db = SessionLocal() try: company = db.query(Company).filter(Company.id == company_id).first() - if not company: return + if not company: + logger.error(f"Analysis Task: Company with ID {company_id} not found.") + return - logger.info(f"Running Analysis Task for {company.name}") + logger.info(f"--- [BACKGROUND TASK] Starting for {company.name} ---") - # 1. Scrape Website (if not locked) + # --- 1. Scrape Website (if not locked) --- existing_scrape = db.query(EnrichmentData).filter( EnrichmentData.company_id == company.id, EnrichmentData.source_type == "website_scrape" ).first() if not existing_scrape or not existing_scrape.is_locked: - from .services.scraping import ScraperService - scrape_res = ScraperService().scrape_url(company.website) + logger.info(f"Scraping website for {company.name}...") + scrape_res = scraper.scrape_url(company.website) if not existing_scrape: db.add(EnrichmentData(company_id=company.id, source_type="website_scrape", content=scrape_res)) + logger.info("Created new website_scrape entry.") else: existing_scrape.content = scrape_res existing_scrape.updated_at = datetime.utcnow() + logger.info("Updated existing website_scrape entry.") db.commit() + else: + logger.info("Website scrape is locked. Skipping.") - # 2. Classify Industry & Metrics - # IMPORTANT: Using the new method name and passing db session + # --- 2. Classify Industry & Metrics --- + logger.info(f"Handing over to ClassificationService for {company.name}...") classifier.classify_company_potential(company, db) company.status = "ENRICHED" db.commit() - logger.info(f"Analysis complete for {company.name}") + logger.info(f"--- [BACKGROUND TASK] Successfully finished for {company.name} ---") + except Exception as e: - logger.error(f"Analyze Task Error: {e}", exc_info=True) + logger.critical(f"--- [BACKGROUND TASK] CRITICAL ERROR for Company ID {company_id} ---", exc_info=True) finally: db.close() +def run_batch_classification_task(): + from .database import SessionLocal + from .lib.core_utils import call_gemini_flash + import json + + db = SessionLocal() + logger.info("--- [BACKGROUND TASK] Starting Batch Job Title Classification ---") + BATCH_SIZE = 50 + + try: + personas = db.query(Persona).all() + available_roles = [p.name for p in personas] + if not available_roles: + logger.error("No Personas found. Aborting classification task.") + return + + unmapped_titles = db.query(RawJobTitle).filter(RawJobTitle.is_mapped == False).all() + if not unmapped_titles: + logger.info("No unmapped titles to process.") + return + + logger.info(f"Found {len(unmapped_titles)} unmapped titles. Processing in batches of {BATCH_SIZE}.") + + for i in range(0, len(unmapped_titles), BATCH_SIZE): + batch = unmapped_titles[i:i + BATCH_SIZE] + title_strings = [item.title for item in batch] + + prompt = f'''You are an expert in B2B contact segmentation. Classify the following job titles into one of the provided roles: {', '.join(available_roles)}. Respond ONLY with a valid JSON object mapping the title to the role. Use "Influencer" as a fallback. Titles: {json.dumps(title_strings)}''' + + response_text = "" + try: + response_text = call_gemini_flash(prompt, json_mode=True) + if response_text.strip().startswith("```json"): + response_text = response_text.strip()[7:-4] + classifications = json.loads(response_text) + except Exception as e: + logger.error(f"LLM response error for batch, skipping. Error: {e}. Response: {response_text}") + continue + + new_patterns = 0 + for title_obj in batch: + original_title = title_obj.title + assigned_role = classifications.get(original_title) + + if assigned_role and assigned_role in available_roles: + if not db.query(JobRolePattern).filter(JobRolePattern.pattern_value == original_title).first(): + db.add(JobRolePattern(pattern_type='exact', pattern_value=original_title, role=assigned_role, priority=90, created_by='llm_batch')) + new_patterns += 1 + title_obj.is_mapped = True + + db.commit() + logger.info(f"Batch {i//BATCH_SIZE + 1} complete. Created {new_patterns} new patterns.") + + except Exception as e: + logger.critical(f"--- [BACKGROUND TASK] CRITICAL ERROR during classification ---", exc_info=True) + db.rollback() + finally: + db.close() + logger.info("--- [BACKGROUND TASK] Finished Batch Job Title Classification ---") + # --- Serve Frontend --- static_path = "/frontend_static" if not os.path.exists(static_path): - # Local dev fallback static_path = os.path.join(os.path.dirname(__file__), "../../frontend/dist") if not os.path.exists(static_path): static_path = os.path.join(os.path.dirname(__file__), "../static") @@ -823,11 +1620,34 @@ if not os.path.exists(static_path): logger.info(f"Static files path: {static_path} (Exists: {os.path.exists(static_path)})") if os.path.exists(static_path): + from fastapi.responses import FileResponse + from fastapi.staticfiles import StaticFiles + + index_file = os.path.join(static_path, "index.html") + + # Mount assets specifically first + assets_path = os.path.join(static_path, "assets") + if os.path.exists(assets_path): + app.mount("/assets", StaticFiles(directory=assets_path), name="assets") + @app.get("/") async def serve_index(): - return FileResponse(os.path.join(static_path, "index.html")) + return FileResponse(index_file) - app.mount("/", StaticFiles(directory=static_path, html=True), name="static") + # Catch-all for SPA routing (any path not matched by API or assets) + @app.get("/{full_path:path}") + async def spa_fallback(full_path: str): + # Allow API calls to fail naturally with 404 + if full_path.startswith("api/"): + raise HTTPException(status_code=404) + + # If it's a file that exists, serve it (e.g. favicon, robots.txt) + file_path = os.path.join(static_path, full_path) + if os.path.isfile(file_path): + return FileResponse(file_path) + + # Otherwise, serve index.html for SPA routing + return FileResponse(index_file) else: @app.get("/") def root_no_frontend(): diff --git a/company-explorer/backend/config.py b/company-explorer/backend/config.py index ad5250d3..d78f842d 100644 --- a/company-explorer/backend/config.py +++ b/company-explorer/backend/config.py @@ -14,7 +14,7 @@ try: DEBUG: bool = True # Database (FINAL CORRECT PATH for Docker Container) - DATABASE_URL: str = "sqlite:////app/companies_v3_fixed_2.db" + DATABASE_URL: str = "sqlite:////data/companies_v3_fixed_2.db" # API Keys GEMINI_API_KEY: Optional[str] = None @@ -22,7 +22,10 @@ try: SERP_API_KEY: Optional[str] = None # Paths - LOG_DIR: str = "/app/logs_debug" + LOG_DIR: str = "/app/Log_from_docker" + + # Public URL + APP_BASE_URL: str = "http://localhost:8090" class Config: env_file = ".env" @@ -36,11 +39,12 @@ except ImportError: APP_NAME = "Company Explorer" VERSION = "0.7.3" DEBUG = True - DATABASE_URL = "sqlite:////app/companies_v3_fixed_2.db" # FINAL CORRECT PATH + # HARDCODED PATH TO FORCE CONSISTENCY + DATABASE_URL = "sqlite:////data/companies_v3_fixed_2.db" GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") SERP_API_KEY = os.getenv("SERP_API_KEY") - LOG_DIR = "/app/logs_debug" + LOG_DIR = "/app/Log_from_docker" settings = FallbackSettings() diff --git a/company-explorer/backend/database.py b/company-explorer/backend/database.py index 2c82e456..4dd88791 100644 --- a/company-explorer/backend/database.py +++ b/company-explorer/backend/database.py @@ -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() \ No newline at end of file diff --git a/company-explorer/backend/lib/metric_parser.py b/company-explorer/backend/lib/metric_parser.py index 1a83c864..b5b55f3b 100644 --- a/company-explorer/backend/lib/metric_parser.py +++ b/company-explorer/backend/lib/metric_parser.py @@ -23,52 +23,43 @@ class MetricParser: # 1. Pre-cleaning text_processed = str(text).strip() - logger.info(f"[MetricParser] Processing: '{text_processed}' (Expected: {expected_value})") + logger.info(f"[MetricParser] Processing text (len: {len(text_processed)}) (Hint: {expected_value})") - # Optimize: If we have an expected value, try to clean and parse THAT first + # Optimize: If we have an expected value (hint), try to find that specific number first if expected_value: - # Try to parse the LLM's raw value directly first (it's often cleaner: "200000") - try: - # Remove simple noise from expected value - # Aggressively strip units and text to isolate the number - clean_expected = str(expected_value).lower() - # Remove common units - for unit in ['m²', 'qm', 'sqm', 'mitarbeiter', 'employees', 'eur', 'usd', 'chf', '€', '$', '£', '¥']: - clean_expected = clean_expected.replace(unit, "") - - # Remove multipliers text (we handle multipliers via is_revenue later, but for expected value matching we want the raw number) - # Actually, expected_value "2.5 Mio" implies we want to match 2.5 in the text, OR 2500000? - # Usually the LLM extract matches the text representation. - clean_expected = clean_expected.replace("mio", "").replace("millionen", "").replace("mrd", "").replace("milliarden", "") - clean_expected = clean_expected.replace("tsd", "").replace("tausend", "") - - # Final cleanup of non-numeric chars (allow . , ' -) - # But preserve structure for robust parser - clean_expected = clean_expected.replace(" ", "").replace("'", "") - - # If it looks like a clean number already, try parsing it - # But use the robust parser to handle German decimals if present in expected - val = MetricParser._parse_robust_number(clean_expected, is_revenue) - - # Check if this value (or a close representation) actually exists in the text - # This prevents hallucination acceptance, but allows the LLM to guide us to the *second* number in a string. - # Simplified check: is the digits sequence present? - # No, better: Let the parser run on the FULL text, find all candidates, and pick the one closest to 'val'. - except: - pass + try: + # Clean the hint to get the target digits (e.g. "352" from "352 Betten") + # We only take the FIRST sequence of digits as the target + hint_match = re.search(r'[\d\.,\']+', str(expected_value)) + if hint_match: + target_str = hint_match.group(0) + target_digits = re.sub(r'[^0-9]', '', target_str) + + if target_digits: + # Find all numbers in the text and check if they match our target + all_numbers_in_text = re.findall(r'[\d\.,\']+', text_processed) + for num_str in all_numbers_in_text: + if target_digits == re.sub(r'[^0-9]', '', num_str): + # Exact digit match! + val = MetricParser._parse_robust_number(num_str, is_revenue) + if val is not None: + logger.info(f"[MetricParser] Found targeted value via hint: '{num_str}' -> {val}") + return val + except Exception as e: + logger.error(f"Error while parsing with hint: {e}") + # Fallback: Classic robust parsing # Normalize quotes text_processed = text_processed.replace("’", "'").replace("‘", "'") # 2. Remove noise: Citations [1] and Year/Date in parentheses (2020) - # We remove everything in parentheses/brackets as it's almost always noise for the metric itself. text_processed = re.sub(r'\(.*?\)|\[.*?\]', ' ', text_processed).strip() # 3. Remove common prefixes and currency symbols prefixes = [ - r'ca\.?\s*', r'circa\s*', r'rund\s*', r'etwa\s*', r'über\s*', r'unter\s*', + r'ca\.?:?\s*', r'circa\s*', r'rund\s*', r'etwa\s*', r'über\s*', r'unter\s*', r'mehr als\s*', r'weniger als\s*', r'bis zu\s*', r'about\s*', r'over\s*', - r'approx\.?\s*', r'around\s*', r'up to\s*', r'~\s*', r'rd\.?\s*' + r'approx\.?:?\s*', r'around\s*', r'up to\s*', r'~\s*', r'rd\.?:?\s*' ] currencies = [ r'€', r'EUR', r'US\$', r'USD', r'CHF', r'GBP', r'£', r'¥', r'JPY' @@ -79,23 +70,16 @@ class MetricParser: for c in currencies: text_processed = re.sub(f'(?i){c}', '', text_processed).strip() - # 4. Remove Range Splitting (was too aggressive, cutting off text after dashes) - # Old: text_processed = re.split(r'\s*(-|–|bis|to)\s*', text_processed, 1)[0].strip() - - # 5. Extract Multipliers (Mio, Mrd) + # 4. Extract Multipliers (Mio, Mrd) multiplier = 1.0 lower_text = text_processed.lower() def has_unit(text, units): for u in units: - # Escape special chars if any, though mostly alphanumeric here - # Use word boundaries \b for safe matching if re.search(r'\b' + re.escape(u) + r'\b', text): return True return False - # For Revenue, we normalize to Millions (User Rule) - # For others (Employees), we scale to absolute numbers if is_revenue: if has_unit(lower_text, ['mrd', 'milliarden', 'billion', 'bn']): multiplier = 1000.0 @@ -111,214 +95,92 @@ class MetricParser: elif has_unit(lower_text, ['tsd', 'tausend', 'k']): multiplier = 1000.0 - # 6. Extract the number candidate - # Loop through matches to find the best candidate (skipping years if possible) + # 5. Extract the first valid number candidate candidates = re.finditer(r'([\d\.,\'\s]+)', text_processed) - selected_candidate = None - best_candidate_val = None - - matches = [m for m in candidates] - # logger.info(f"DEBUG matches: {[m.group(1) for m in matches]}") - # logger.info(f"DEBUG: Found {len(matches)} matches: {[m.group(1) for m in matches]}") - - # Helper to parse a candidate string - def parse_cand(c): - # Extract temporary multiplier for this specific candidate context? - # Complex. For now, we assume the global multiplier applies or we rely on the candidates raw numeric value. - # Actually, simpler: We parse the candidate as is (treating as raw number) - try: - # Remove thousands separators for comparison - c_clean = c.replace("'", "").replace(".", "").replace(" ", "").replace(",", ".") # Rough EN/DE mix - return float(c_clean) - except: - return None - - # Parse expected value for comparison - target_val = None - if expected_value: - try: - # Re-apply aggressive cleaning to ensure we have a valid float for comparison - clean_expected = str(expected_value).lower() - for unit in ['m²', 'qm', 'sqm', 'mitarbeiter', 'employees', 'eur', 'usd', 'chf', '€', '$', '£', '¥']: - clean_expected = clean_expected.replace(unit, "") - clean_expected = clean_expected.replace("mio", "").replace("millionen", "").replace("mrd", "").replace("milliarden", "") - clean_expected = clean_expected.replace("tsd", "").replace("tausend", "") - clean_expected = clean_expected.replace(" ", "").replace("'", "") - - target_val = MetricParser._parse_robust_number(clean_expected, is_revenue) - except: - pass - - for i, match in enumerate(matches): + for match in candidates: cand = match.group(1).strip() - if not cand: continue + if not cand or not re.search(r'\d', cand): + continue - # Clean candidate for analysis (remove separators) + # Clean candidate clean_cand = cand.replace("'", "").replace(".", "").replace(",", "").replace(" ", "") - # Check if it looks like a year (4 digits, 1900-2100) - is_year_like = False + # Year detection if clean_cand.isdigit() and len(clean_cand) == 4: val = int(clean_cand) if 1900 <= val <= 2100: - is_year_like = True + continue # Skip years - # Smart Year Skip (Legacy Logic) - if is_year_like and not target_val: # Only skip if we don't have a specific target - if i < len(matches) - 1: - logger.info(f"[MetricParser] Skipping year-like candidate '{cand}' because another number follows.") - continue - - # Clean candidate for checking (remove internal spaces if they look like thousands separators) - # Simple approach: Remove all spaces for parsing check - cand_clean_for_parse = cand.replace(" ", "") - - # If we have a target value from LLM, check if this candidate matches it - if target_val is not None: - try: - curr_val = MetricParser._parse_robust_number(cand_clean_for_parse, is_revenue) - - if abs(curr_val - target_val) < 0.1 or abs(curr_val - target_val/1000) < 0.1 or abs(curr_val - target_val*1000) < 0.1: - selected_candidate = cand # Keep original with spaces for final processing - logger.info(f"[MetricParser] Found candidate '{cand}' matching expected '{expected_value}'") - break - except: - pass - - # Fallback logic: - # If we have NO target value, we take the first valid one we find. - # If we DO have a target value, we only take a fallback if we reach the end and haven't found the target? - # Better: We keep the FIRST valid candidate as a fallback in a separate variable. - - if selected_candidate is None: - # Check if it's a valid number at all before storing as fallback - try: - MetricParser._parse_robust_number(cand_clean_for_parse, is_revenue) - if not is_year_like: - if best_candidate_val is None: # Store first valid non-year - best_candidate_val = cand - except: - pass + # Smart separator handling for spaces + if " " in cand: + parts = cand.split() + if len(parts) > 1: + if not (len(parts[1]) == 3 and parts[1].isdigit()): + cand = parts[0] + else: + merged = parts[0] + for p in parts[1:]: + if len(p) == 3 and p.isdigit(): + merged += p + else: + break + cand = merged - # If we found a specific match, use it. Otherwise use the fallback. - if selected_candidate: - candidate = selected_candidate - elif best_candidate_val: - candidate = best_candidate_val - else: - return None - - # logger.info(f"DEBUG: Selected candidate: '{candidate}'") - - # Smart separator handling (on the chosen candidate): - - # Smart separator handling: - - # Smart separator handling: - # A space is only a thousands-separator if it's followed by 3 digits. - # Otherwise it's likely a separator between unrelated numbers (e.g. "80 2020") - if " " in candidate: - parts = candidate.split() - if len(parts) > 1: - # Basic check: if second part is not 3 digits, we take only the first part - if not (len(parts[1]) == 3 and parts[1].isdigit()): - candidate = parts[0] - else: - # It might be 1 000. Keep merging if subsequent parts are also 3 digits. - merged = parts[0] - for p in parts[1:]: - if len(p) == 3 and p.isdigit(): - merged += p - else: - break - candidate = merged - - # Remove thousands separators (Quote) - candidate = candidate.replace("'", "") - - if not candidate or not re.search(r'\d', candidate): - return None + try: + val = MetricParser._parse_robust_number(cand, is_revenue) + if val is not None: + final = val * multiplier + logger.info(f"[MetricParser] Found value: '{cand}' -> {final}") + return final + except: + continue - # Count separators for rule checks - dots = candidate.count('.') - commas = candidate.count(',') - - # 7. Concatenated Year Detection (Bug Fix for 802020) - # If the number is long (5-7 digits) and ends with a recent year (2018-2026), - # and has no separators, it's likely a concatenation like "802020". - if dots == 0 and commas == 0 and " " not in candidate: - if len(candidate) >= 5 and len(candidate) <= 7: - for year in range(2018, 2027): - y_str = str(year) - if candidate.endswith(y_str): - val_str = candidate[:-4] - if val_str.isdigit(): - logger.warning(f"[MetricParser] Caught concatenated year BUG: '{candidate}' -> '{val_str}' (Year {year})") - candidate = val_str - break - - try: - val = MetricParser._parse_robust_number(candidate, is_revenue) - final = val * multiplier - logger.info(f"[MetricParser] Candidate: '{candidate}' -> Multiplier: {multiplier} -> Value: {final}") - return final - except Exception as e: - logger.debug(f"Failed to parse number string '{candidate}': {e}") - return None + return None @staticmethod - def _parse_robust_number(s: str, is_revenue: bool) -> float: + def _parse_robust_number(s: str, is_revenue: bool) -> Optional[float]: """ Parses a number string dealing with ambiguous separators. Standardizes to Python float. """ - # Count separators + s = s.strip().replace("'", "") + if not s: + return None + dots = s.count('.') commas = s.count(',') - # Case 1: Both present (e.g. 1.234,56 or 1,234.56) - if dots > 0 and commas > 0: - # Check which comes last - if s.rfind('.') > s.rfind(','): # US Style: 1,234.56 + try: + # Case 1: Both present + if dots > 0 and commas > 0: + if s.rfind('.') > s.rfind(','): # US Style + return float(s.replace(',', '')) + else: # German Style + return float(s.replace('.', '').replace(',', '.')) + + # Case 2: Multiple dots + if dots > 1: + return float(s.replace('.', '')) + + # Case 3: Multiple commas + if commas > 1: return float(s.replace(',', '')) - else: # German Style: 1.234,56 - return float(s.replace('.', '').replace(',', '.')) - - # Case 2: Multiple dots (Thousands: 1.000.000) - if dots > 1: - return float(s.replace('.', '')) - - # Case 3: Multiple commas (Unusual, but treat as thousands) - if commas > 1: - return float(s.replace(',', '')) - # Case 4: Only Comma - if commas == 1: - # In German context "1,5" is 1.5. "1.000" is usually 1000. - # If it looks like decimal (1-2 digits after comma), treat as decimal. - # Except if it's exactly 3 digits and not is_revenue? No, comma is almost always decimal in DE. - return float(s.replace(',', '.')) - - # Case 5: Only Dot - if dots == 1: - # Ambiguity: "1.005" (1005) vs "1.5" (1.5) - # Rule from Lesson 1: "1.005 Mitarbeiter" extracted as "1" (wrong). - # If dot followed by exactly 3 digits (and no comma), it's a thousands separator. - # FOR REVENUE: dots are generally decimals (375.6 Mio) unless unambiguous. + # Case 4: Only Comma + if commas == 1: + return float(s.replace(',', '.')) - parts = s.split('.') - if len(parts[1]) == 3: - if is_revenue: - # Revenue: 375.600 Mio? Unlikely compared to 375.6 Mio. - # But 1.000 Mio is 1 Billion? No, 1.000 (thousand) millions. - # User Rule: "Revenue: dots are generally treated as decimals" - # "1.005" as revenue -> 1.005 (Millions) - # "1.005" as employees -> 1005 - return float(s) - else: - return float(s.replace('.', '')) + # Case 5: Only Dot + if dots == 1: + parts = s.split('.') + if len(parts[1]) == 3: + if is_revenue: + return float(s) + else: + return float(s.replace('.', '')) + return float(s) + return float(s) - - return float(s) - + except: + return None \ No newline at end of file diff --git a/company-explorer/backend/scripts/__init__.py b/company-explorer/backend/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/company-explorer/backend/scripts/add_unsubscribe_tokens.py b/company-explorer/backend/scripts/add_unsubscribe_tokens.py new file mode 100644 index 00000000..4dbb7784 --- /dev/null +++ b/company-explorer/backend/scripts/add_unsubscribe_tokens.py @@ -0,0 +1,44 @@ +import uuid +import os +import sys + +# This is the crucial part to fix the import error. +# We add the 'company-explorer' directory to the path, so imports can be absolute +# from the 'backend' module. +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) + +from backend.database import Contact, SessionLocal + +def migrate_existing_contacts(): + """ + Generates and adds an unsubscribe_token for all existing contacts + that do not have one yet. + """ + db = SessionLocal() + try: + contacts_to_update = db.query(Contact).filter(Contact.unsubscribe_token == None).all() + + if not contacts_to_update: + print("All contacts already have an unsubscribe token. No migration needed.") + return + + print(f"Found {len(contacts_to_update)} contacts without an unsubscribe token. Generating tokens...") + + for contact in contacts_to_update: + token = str(uuid.uuid4()) + contact.unsubscribe_token = token + print(f" - Generated token for contact ID {contact.id} ({contact.email})") + + db.commit() + print("\nSuccessfully updated all contacts with new unsubscribe tokens.") + + except Exception as e: + print(f"An error occurred: {e}") + db.rollback() + finally: + db.close() + +if __name__ == "__main__": + print("Starting migration: Populating unsubscribe_token for existing contacts.") + migrate_existing_contacts() + print("Migration finished.") \ No newline at end of file diff --git a/company-explorer/backend/scripts/analyze_job_title_patterns.py b/company-explorer/backend/scripts/analyze_job_title_patterns.py new file mode 100644 index 00000000..ceb7f9aa --- /dev/null +++ b/company-explorer/backend/scripts/analyze_job_title_patterns.py @@ -0,0 +1,82 @@ +import sys +import os +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from collections import Counter +import re + +# Add backend to path to import models +sys.path.append(os.path.join(os.path.dirname(__file__), "../..")) + +from backend.config import settings +from backend.database import Contact, JobRolePattern + +def clean_text(text): + if not text: return "" + # Keep only alphanumeric and spaces + text = re.sub(r'[^\w\s]', ' ', text) + return text.lower().strip() + +def get_ngrams(tokens, n): + if len(tokens) < n: + return [] + return [" ".join(tokens[i:i+n]) for i in range(len(tokens)-n+1)] + +def analyze_patterns(): + print(f"Connecting to database: {settings.DATABASE_URL}") + engine = create_engine(settings.DATABASE_URL) + Session = sessionmaker(bind=engine) + session = Session() + + try: + # Fetch all contacts with a role + contacts = session.query(Contact).filter(Contact.role != None, Contact.job_title != None).all() + print(f"Found {len(contacts)} classified contacts to analyze.") + + role_groups = {} + for c in contacts: + if c.role not in role_groups: + role_groups[c.role] = [] + role_groups[c.role].append(c.job_title) + + print("\n" + "="*60) + print(" JOB TITLE PATTERN ANALYSIS REPORT") + print("="*60 + "\n") + + for role, titles in role_groups.items(): + print(f"--- ROLE: {role} ({len(titles)} samples) ---") + + # Tokenize all titles + all_tokens = [] + all_bigrams = [] + + for t in titles: + cleaned = clean_text(t) + tokens = [w for w in cleaned.split() if len(w) > 2] # Ignore short words + all_tokens.extend(tokens) + all_bigrams.extend(get_ngrams(tokens, 2)) + + # Analyze frequencies + common_words = Counter(all_tokens).most_common(15) + common_bigrams = Counter(all_bigrams).most_common(10) + + print("Top Keywords:") + for word, count in common_words: + print(f" - {word}: {count}") + + print("\nTop Bigrams (Word Pairs):") + for bg, count in common_bigrams: + print(f" - \"{bg}\": {count}") + + print("\nSuggested Regex Components:") + top_5_words = [w[0] for w in common_words[:5]] + print(f" ({ '|'.join(top_5_words) })") + print("\n" + "-"*30 + "\n") + + except Exception as e: + print(f"Error: {e}") + finally: + session.close() + +if __name__ == "__main__": + analyze_patterns() diff --git a/company-explorer/backend/scripts/check_mappings.py b/company-explorer/backend/scripts/check_mappings.py new file mode 100644 index 00000000..65be09b4 --- /dev/null +++ b/company-explorer/backend/scripts/check_mappings.py @@ -0,0 +1,22 @@ + +import sys +import os + +# Setup Environment +sys.path.append(os.path.join(os.path.dirname(__file__), "../../")) + +from backend.database import SessionLocal, JobRolePattern + +def check_mappings(): + db = SessionLocal() + count = db.query(JobRolePattern).count() + print(f"Total JobRolePatterns: {count}") + + examples = db.query(JobRolePattern).limit(5).all() + for ex in examples: + print(f" - {ex.pattern} -> {ex.role}") + + db.close() + +if __name__ == "__main__": + check_mappings() diff --git a/company-explorer/backend/scripts/classify_unmapped_titles.py b/company-explorer/backend/scripts/classify_unmapped_titles.py new file mode 100644 index 00000000..a09a3631 --- /dev/null +++ b/company-explorer/backend/scripts/classify_unmapped_titles.py @@ -0,0 +1,171 @@ +import sys +import os +import argparse +import json +import logging +from sqlalchemy.orm import sessionmaker, declarative_base +from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime +from datetime import datetime + +# --- Standalone Configuration --- +# Add the project root to the Python path to find the LLM utility +sys.path.insert(0, '/app') +from company_explorer.backend.lib.core_utils import call_gemini_flash + +DATABASE_URL = "sqlite:////app/companies_v3_fixed_2.db" +LOG_FILE = "/app/Log_from_docker/batch_classifier.log" +BATCH_SIZE = 50 # Number of titles to process in one LLM call + +# --- Logging Setup --- +os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True) +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(LOG_FILE), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# --- SQLAlchemy Models (self-contained) --- +Base = declarative_base() + +class RawJobTitle(Base): + __tablename__ = 'raw_job_titles' + id = Column(Integer, primary_key=True) + title = Column(String, unique=True, index=True) + count = Column(Integer, default=1) + source = Column(String) + is_mapped = Column(Boolean, default=False) + created_at = Column(DateTime, default=datetime.now) + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) + +class JobRolePattern(Base): + __tablename__ = "job_role_patterns" + id = Column(Integer, primary_key=True, index=True) + pattern_type = Column(String, default="exact", index=True) + pattern_value = Column(String, unique=True) + role = Column(String, index=True) + priority = Column(Integer, default=100) + is_active = Column(Boolean, default=True) + created_by = Column(String, default="system") + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + +class Persona(Base): + __tablename__ = "personas" + id = Column(Integer, primary_key=True, index=True) + name = Column(String, unique=True, index=True) + pains = Column(String) + gains = Column(String) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + +# --- Database Connection --- +engine = create_engine(DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +def build_classification_prompt(titles_to_classify, available_roles): + """Builds the prompt for the LLM to classify a batch of job titles.""" + prompt = f""" + You are an expert in B2B contact segmentation. Your task is to classify a list of job titles into predefined roles. + + Analyze the following list of job titles and assign each one to the most appropriate role from the list provided. + + The available roles are: + - {', '.join(available_roles)} + + RULES: + 1. Respond ONLY with a valid JSON object. Do not include any text, explanations, or markdown code fences before or after the JSON. + 2. The JSON object should have the original job title as the key and the assigned role as the value. + 3. If a job title is ambiguous or you cannot confidently classify it, assign the value "Influencer". Use this as a fallback. + 4. Do not invent new roles. Only use the roles from the provided list. + + Here are the job titles to classify: + {json.dumps(titles_to_classify, indent=2)} + + Your JSON response: + """ + return prompt + +def classify_and_store_titles(): + db = SessionLocal() + try: + # 1. Fetch available persona names (roles) + personas = db.query(Persona).all() + available_roles = [p.name for p in personas] + if not available_roles: + logger.error("No Personas/Roles found in the database. Cannot classify. Please seed personas first.") + return + + logger.info(f"Classifying based on these roles: {available_roles}") + + # 2. Fetch unmapped titles + unmapped_titles = db.query(RawJobTitle).filter(RawJobTitle.is_mapped == False).all() + if not unmapped_titles: + logger.info("No unmapped job titles found. Nothing to do.") + return + + logger.info(f"Found {len(unmapped_titles)} unmapped job titles to process.") + + # 3. Process in batches + for i in range(0, len(unmapped_titles), BATCH_SIZE): + batch = unmapped_titles[i:i + BATCH_SIZE] + title_strings = [item.title for item in batch] + + logger.info(f"Processing batch {i//BATCH_SIZE + 1} of { (len(unmapped_titles) + BATCH_SIZE - 1) // BATCH_SIZE } with {len(title_strings)} titles...") + + # 4. Call LLM + prompt = build_classification_prompt(title_strings, available_roles) + response_text = "" + try: + response_text = call_gemini_flash(prompt, json_mode=True) + # Clean potential markdown fences + if response_text.strip().startswith("```json"): + response_text = response_text.strip()[7:-4] + + classifications = json.loads(response_text) + except Exception as e: + logger.error(f"Failed to get or parse LLM response for batch. Skipping. Error: {e}") + logger.error(f"Raw response was: {response_text}") + continue + + # 5. Process results + new_patterns = 0 + for title_obj in batch: + original_title = title_obj.title + assigned_role = classifications.get(original_title) + + if assigned_role and assigned_role in available_roles: + exists = db.query(JobRolePattern).filter(JobRolePattern.pattern_value == original_title).first() + if not exists: + new_pattern = JobRolePattern( + pattern_type='exact', + pattern_value=original_title, + role=assigned_role, + priority=90, + created_by='llm_batch' + ) + db.add(new_pattern) + new_patterns += 1 + title_obj.is_mapped = True + else: + logger.warning(f"Could not classify '{original_title}' or role '{assigned_role}' is invalid. It will be re-processed later.") + + db.commit() + logger.info(f"Batch {i//BATCH_SIZE + 1} complete. Created {new_patterns} new mapping patterns.") + + except Exception as e: + logger.error(f"An unexpected error occurred: {e}", exc_info=True) + db.rollback() + finally: + db.close() + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Batch classify unmapped job titles using an LLM.") + args = parser.parse_args() + + logger.info("--- Starting Batch Classification Script ---") + classify_and_store_titles() + logger.info("--- Batch Classification Script Finished ---") \ No newline at end of file diff --git a/company-explorer/backend/scripts/debug_check_matrix_texts.py b/company-explorer/backend/scripts/debug_check_matrix_texts.py new file mode 100644 index 00000000..5cab6144 --- /dev/null +++ b/company-explorer/backend/scripts/debug_check_matrix_texts.py @@ -0,0 +1,64 @@ + +import sys +import os +import argparse + +# Adjust the path to include the 'company-explorer' directory +# This allows the script to find the 'backend' module +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + +from backend.database import SessionLocal, Industry, Persona, MarketingMatrix + +def check_texts_for_industry(industry_name: str): + """ + Fetches and prints the marketing matrix texts for all personas + within a specific industry. + """ + db = SessionLocal() + try: + industry = db.query(Industry).filter(Industry.name == industry_name).first() + + if not industry: + print(f"Error: Industry '{industry_name}' not found.") + # Suggest similar names + all_industries = db.query(Industry.name).all() + print("\nAvailable Industries:") + for (name,) in all_industries: + print(f"- {name}") + return + + entries = ( + db.query(MarketingMatrix) + .join(Persona) + .filter(MarketingMatrix.industry_id == industry.id) + .order_by(Persona.id) + .all() + ) + + if not entries: + print(f"No marketing texts found for industry: {industry_name}") + return + + print(f"--- NEW TEXTS FOR {industry_name} ---") + for entry in entries: + print(f"\nPERSONA: {entry.persona.name}") + print(f"Subject: {entry.subject}") + print(f"Intro: {entry.intro.replace(chr(10), ' ')}") # Replace newlines for cleaner one-line output + + except Exception as e: + print(f"An error occurred: {e}") + finally: + db.close() + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Debug and check marketing matrix texts for a given industry.") + parser.add_argument( + "industry", + type=str, + nargs='?', + default="Healthcare - Hospital", + help="The name of the industry to check (e.g., 'Healthcare - Hospital'). Defaults to 'Healthcare - Hospital'." + ) + args = parser.parse_args() + + check_texts_for_industry(args.industry) diff --git a/company-explorer/backend/scripts/debug_single_company.py b/company-explorer/backend/scripts/debug_single_company.py new file mode 100644 index 00000000..9b651d3b --- /dev/null +++ b/company-explorer/backend/scripts/debug_single_company.py @@ -0,0 +1,94 @@ +import os +import sys +import argparse +import logging +from typing import Any # Hinzugefügt + +# Add the company-explorer directory to the Python path +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + +from backend.database import get_db, Company, Industry, Persona # Added Industry and Persona for full context +from backend.services.classification import ClassificationService +from backend.lib.logging_setup import setup_logging + +# --- CONFIGURATION --- +# Setup logging to be very verbose for this script +setup_logging() +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +def run_debug_analysis(company_identifier: Any, is_id: bool): + """ + Runs the full classification and enrichment process for a single company + in the foreground and prints detailed results. + """ + logger.info(f"--- Starting Interactive Debug for Company: {company_identifier} (by {'ID' if is_id else 'Name'}) ---") + + db_session = next(get_db()) + + try: + # 1. Fetch the company + if is_id: + company = db_session.query(Company).filter(Company.id == company_identifier).first() + else: + company = db_session.query(Company).filter(Company.name == company_identifier).first() + + if not company: + logger.error(f"Company with {'ID' if is_id else 'Name'} {company_identifier} not found.") + # If by name, suggest similar names + if not is_id: + all_company_names = db_session.query(Company.name).limit(20).all() + print("\nAvailable Company Names (first 20):") + for (name,) in all_company_names: + print(f"- {name}") + return + + logger.info(f"Found Company: {company.name} (ID: {company.id})") + + # --- PRE-ANALYSIS STATE --- + print("\n--- METRICS BEFORE ---") + print(f"Calculated: {company.calculated_metric_value} {company.calculated_metric_unit}") + print(f"Standardized: {company.standardized_metric_value} {company.standardized_metric_unit}") + print(f"Opener 1 (Infra): {company.ai_opener}") + print(f"Opener 2 (Ops): {company.ai_opener_secondary}") + print("----------------------\n") + + # 2. Instantiate the service + classifier = ClassificationService() + + # 3. RUN THE CORE LOGIC + # This will now print all the detailed logs we added + updated_company = classifier.classify_company_potential(company, db_session) + + # --- POST-ANALYSIS STATE --- + print("\n--- METRICS AFTER ---") + print(f"Industry (AI): {updated_company.industry_ai}") + print(f"Metric Source: {updated_company.metric_source}") + print(f"Proof Text: {updated_company.metric_proof_text}") + print(f"Calculated: {updated_company.calculated_metric_value} {updated_company.calculated_metric_unit}") + print(f"Standardized: {updated_company.standardized_metric_value} {updated_company.standardized_metric_unit}") + print(f"\nOpener 1 (Infra): {updated_company.ai_opener}") + print(f"Opener 2 (Ops): {updated_company.ai_opener_secondary}") + print("---------------------") + + logger.info(f"--- Interactive Debug Finished for Company: {company.name} (ID: {company.id}) ---") + + except Exception as e: + logger.error(f"An error occurred during analysis: {e}", exc_info=True) + finally: + db_session.close() + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Run a single company analysis for debugging.") + parser.add_argument("--id", type=int, help="The ID of the company to analyze.") + parser.add_argument("--company-name", type=str, help="The name of the company to analyze.") + args = parser.parse_args() + + if args.id and args.company_name: + parser.error("Please provide either --id or --company-name, not both.") + elif args.id: + run_debug_analysis(args.id, is_id=True) + elif args.company_name: + run_debug_analysis(args.company_name, is_id=False) + else: + parser.error("Please provide either --id or --company-name.") diff --git a/company-explorer/backend/scripts/generate_matrix.py b/company-explorer/backend/scripts/generate_matrix.py new file mode 100644 index 00000000..8a1aca8a --- /dev/null +++ b/company-explorer/backend/scripts/generate_matrix.py @@ -0,0 +1,307 @@ +import sys +import os +import json +import argparse +import re +import google.generativeai as genai + +# Setup Environment +sys.path.append(os.path.join(os.path.dirname(__file__), "../../")) + +from backend.database import SessionLocal, Industry, Persona, MarketingMatrix +from backend.config import settings + +# --- Configuration --- +MODEL_NAME = "gemini-2.0-flash" # High quality copy + +def extract_segment(text: str, marker: str) -> str: + """ + Extracts a text block starting with [marker]. + Example: [Primary Product: Cleaning] ... [Secondary Product: Service] + """ + if not text: return "" + + # Split by square brackets that look like headers [Text: ...] + # We look for the marker inside the header + # Simplified Regex: Capture everything inside brackets as ONE group + pattern = r'\[(.*?)\]' + segments = re.split(pattern, text) + + # segments[0] is text before first bracket + # segments[1] is content of first bracket (header) + # segments[2] is content after first bracket (body) + # ... + + best_match = "" + + for i in range(1, len(segments), 2): + header = segments[i] + content = segments[i+1] + + # print(f"DEBUG: Checking Header: '{header}' for Marker: '{marker}'") # Uncomment for deep debug + + if marker.lower() in header.lower(): + return content.strip() + + # Fallback: If no markers found, return full text (legacy support) + if "Primary Product" not in text and "Secondary Product" not in text: + return text + + return "" + +def generate_prompt(industry: Industry, persona: Persona) -> str: + """ + Builds the prompt for the AI to generate the marketing texts. + Combines Industry context with Persona specific pains/gains and Product Category. + """ + + # 1. Determine Product Focus Strategy + # Default: Primary + target_scope = "Primary Product" + target_category = industry.primary_category + + # Special Rule: "Operativer Entscheider" gets Secondary Product IF ops_focus_secondary is True + # Logic: A Nursing Director (Ops) doesn't care about floor cleaning (Facility), + # but cares about Service Robots (Secondary). + if persona.name == "Operativer Entscheider" and industry.ops_focus_secondary: + target_scope = "Secondary Product" + target_category = industry.secondary_category + print(f" -> STRATEGY SWITCH: Using {target_scope} for {persona.name}") + + # Fallback if secondary was requested but not defined + if not target_category: + target_category = industry.primary_category + target_scope = "Primary Product" # Fallback to primary if secondary category object is missing + + product_context = f"{target_category.name}: {target_category.description}" if target_category else "Intelligente Robotik-Lösungen" + + # 2. Extract specific segments from industry pains/gains based on scope + industry_pains = extract_segment(industry.pains, target_scope) + industry_gains = extract_segment(industry.gains, target_scope) + + # Fallback: If specific scope is empty (e.g. no Secondary Pains defined), try Primary + if not industry_pains and target_scope == "Secondary Product": + print(f" -> WARNING: No specific Pains found for {target_scope}. Fallback to Primary.") + industry_pains = extract_segment(industry.pains, "Primary Product") + industry_gains = extract_segment(industry.gains, "Primary Product") + + # 3. Handle Persona Data + try: + persona_pains = json.loads(persona.pains) if persona.pains else [] + persona_gains = json.loads(persona.gains) if persona.gains else [] + except: + persona_pains = [persona.pains] if persona.pains else [] + persona_gains = [persona.gains] if persona.gains else [] + + # Advanced Persona Context + persona_context = f""" +BESCHREIBUNG/DENKWEISE: {persona.description or 'Nicht definiert'} +WAS DIESE PERSON ÜBERZEUGT: {persona.convincing_arguments or 'Nicht definiert'} +RELEVANTE KPIs: {persona.kpis or 'Nicht definiert'} +""" + + prompt = f""" +Du bist ein kompetenter Lösungsberater und brillanter Texter für B2B-Marketing. +AUFGABE: Erstelle 3 hoch-personalisierte Textblöcke (Subject, Introduction_Textonly, Industry_References_Textonly) für eine E-Mail an einen Entscheider. + +--- KONTEXT --- +ZIELBRANCHE: {industry.name} +BRANCHEN-HERAUSFORDERUNGEN (PAIN POINTS): +{industry_pains} + +FOKUS-PRODUKT (LÖSUNG): +{product_context} + +ANSPRECHPARTNER (ROLLE): {persona.name} +{persona_context} + +SPEZIFISCHE HERAUSFORDERUNGEN (PAIN POINTS) DER ROLLE: +{chr(10).join(['- ' + str(p) for p in persona_pains])} + +SPEZIFISCHE NUTZEN (GAINS) DER ROLLE: +{chr(10).join(['- ' + str(g) for g in persona_gains])} + +HINTERGRUNDWISSEN & STRATEGIE (Miller Heiman): +{industry.strategy_briefing or 'Kein spezifisches Briefing verfügbar.'} + +--- DEINE AUFGABE --- +Deine Texte müssen "voll ins Zentrum" der Rolle treffen. Vermeide oberflächliche Floskeln. Nutze die Details zur Denkweise, den KPIs und den Überzeugungsargumenten, um eine tiefgreifende Relevanz zu erzeugen. +Nutze das Strategie-Briefing, um typische Einwände vorwegzunehmen oder "Red Flags" zu vermeiden. + +1. **Subject:** Formuliere eine kurze Betreffzeile (max. 6 Wörter). Richte sie **direkt an einem der persönlichen Pain Points** des Ansprechpartners oder dem zentralen Branchen-Pain. Sei scharfsinnig, nicht werblich. + +2. **Introduction_Textonly:** Formuliere einen prägnanten Einleitungstext (max. 2 Sätze). + - **WICHTIG:** Gehe davon aus, dass die spezifische Herausforderung des Kunden bereits im Satz davor [Opener] genannt wurde. **Wiederhole die Herausforderung NICHT.** + - **Satz 1 (Die Lösung & der Gain):** Beginne direkt mit der Lösung. Nenne die im Kontext `FOKUS-PRODUKT` definierte **Produktkategorie** (z.B. "automatisierte Reinigungsroboter") und verbinde sie mit einem Nutzen, der für diese Rolle (siehe `WAS DIESE PERSON ÜBERZEUGT` und `GAINS`) besonders kritisch ist. + - **Satz 2 (Die Relevanz):** Stelle die Relevanz für die Zielperson her, indem du eine ihrer `PERSÖNLICHE HERAUSFORDERUNGEN` oder `KPIs` adressierst. Beispiel: "Für Sie als [Rolle] bedeutet dies vor allem [Nutzen bezogen auf KPI oder Pain]." + +3. **Industry_References_Textonly:** Formuliere einen **strategischen Referenz-Block (ca. 2-3 Sätze)** nach folgendem Muster: + - **Satz 1 (Social Proof):** Beginne direkt mit dem Nutzen, den vergleichbare Unternehmen in der Branche {industry.name} bereits erzielen. (Erfinde keine Firmennamen, sprich von "Führenden Einrichtungen" oder "Vergleichbaren Häusern"). + - **Satz 2 (Rollen-Relevanz):** Schaffe den direkten Nutzen für die Zielperson. Nutze dabei die Informationen aus `BESCHREIBUNG/DENKWEISE`, um den Ton perfekt zu treffen. + +--- BEISPIEL FÜR EINEN PERFEKTEN OUTPUT --- +{{ + "Subject": "Kostenkontrolle im Service", + "Introduction_Textonly": "Genau bei der Optimierung dieser Serviceprozesse können erhebliche Effizienzgewinne erzielt werden. Für Sie als Finanzleiter ist dabei die Sicherstellung der Profitabilität bei gleichzeitiger Kostentransparenz von zentraler Bedeutung.", + "Industry_References_Textonly": "Vergleichbare Unternehmen profitieren bereits massiv von automatisierten Prozessen. Unsere Erfahrung zeigt, dass die grundlegenden Herausforderungen in der Einsatzplanung oft branchenübergreifend ähnlich sind. Dieser Wissensvorsprung hilft uns, Ihre Ziele bei der Kostenkontrolle und Profitabilitätssteigerung besonders effizient zu unterstützen." +}} + +--- FORMAT --- +Antworte NUR mit einem validen JSON-Objekt. Keine Markdown-Blöcke (```json), kein erklärender Text. +Format: +{{ + "subject": "...", + "intro": "...", + "social_proof": "..." +}} +""" + return prompt + +def mock_call(prompt: str): + """Simulates an API call for dry runs.""" + print(f"\n--- [MOCK] GENERATING PROMPT ---\n{prompt[:800]}...\n--------------------------------") + return { + "subject": "[MOCK] Effizienzsteigerung in der Produktion", + "intro": "[MOCK] Als Produktionsleiter wissen Sie, wie teuer Stillstand ist. Unsere Roboter helfen.", + "social_proof": "[MOCK] Ähnliche Betriebe sparten 20% Kosten." + } + +def real_gemini_call(prompt: str): + if not settings.GEMINI_API_KEY: + raise ValueError("GEMINI_API_KEY not set in config/env") + + genai.configure(api_key=settings.GEMINI_API_KEY) + + # Configure Model + generation_config = { + "temperature": 0.7, + "top_p": 0.95, + "top_k": 64, + "max_output_tokens": 1024, + "response_mime_type": "application/json", + } + + model = genai.GenerativeModel( + model_name=MODEL_NAME, + generation_config=generation_config, + ) + + response = model.generate_content(prompt) + + try: + # Clean response if necessary (Gemini usually returns clean JSON with mime_type set, but safety first) + text = response.text.strip() + if text.startswith("```json"): + text = text[7:-3].strip() + elif text.startswith("```"): + text = text[3:-3].strip() + + parsed_json = json.loads(text) + if isinstance(parsed_json, list): + if len(parsed_json) > 0: + return parsed_json[0] + else: + raise ValueError("Empty list returned from API") + return parsed_json + except Exception as e: + print(f"JSON Parse Error: {e}. Raw Response: {response.text}") + raise + +def run_matrix_generation(dry_run: bool = True, force: bool = False, specific_industry: str = None): + db = SessionLocal() + try: + query = db.query(Industry) + if specific_industry: + query = query.filter(Industry.name == specific_industry) + + industries = query.all() + personas = db.query(Persona).all() + + print(f"Found {len(industries)} Industries and {len(personas)} Personas.") + print(f"Mode: {'DRY RUN (No API calls, no DB writes)' if dry_run else 'LIVE - GEMINI GENERATION'}") + + # Pre-load categories to avoid lazy load issues if detached + # (SQLAlchemy session is open, so should be fine, but good practice) + + total_combinations = len(industries) * len(personas) + processed = 0 + + for ind in industries: + print(f"\n>>> Processing Industry: {ind.name} (Ops Secondary: {ind.ops_focus_secondary})") + for pers in personas: + processed += 1 + print(f"[{processed}/{total_combinations}] Check: {ind.name} x {pers.name}") + + # Check existing + existing = db.query(MarketingMatrix).filter( + MarketingMatrix.industry_id == ind.id, + MarketingMatrix.persona_id == pers.id + ).first() + + if existing and not force: + print(f" -> Skipped (Already exists)") + continue + + # Generate + prompt = generate_prompt(ind, pers) + + if dry_run: + result = mock_call(prompt) + else: + try: + result = real_gemini_call(prompt) + + # Normalize Keys (Case-Insensitive) + normalized_result = {} + for k, v in result.items(): + normalized_result[k.lower()] = v + + # Map known variations to standardized keys + if "introduction_textonly" in normalized_result: + normalized_result["intro"] = normalized_result["introduction_textonly"] + if "industry_references_textonly" in normalized_result: + normalized_result["social_proof"] = normalized_result["industry_references_textonly"] + + # Validation using normalized keys + if not normalized_result.get("subject") or not normalized_result.get("intro"): + print(f" -> Invalid result structure. Keys found: {list(result.keys())}") + print(f" -> Raw Result: {json.dumps(result, indent=2)}") + continue + + except Exception as e: + print(f" -> API ERROR: {e}") + continue + + # Write to DB (only if not dry run) + if not dry_run: + if not existing: + new_entry = MarketingMatrix( + industry_id=ind.id, + persona_id=pers.id, + subject=normalized_result.get("subject"), + intro=normalized_result.get("intro"), + social_proof=normalized_result.get("social_proof") + ) + db.add(new_entry) + print(f" -> Created new entry.") + else: + existing.subject = normalized_result.get("subject") + existing.intro = normalized_result.get("intro") + existing.social_proof = normalized_result.get("social_proof") + print(f" -> Updated entry.") + + db.commit() + + except Exception as e: + print(f"Error: {e}") + finally: + db.close() + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--live", action="store_true", help="Actually call Gemini and write to DB") + parser.add_argument("--force", action="store_true", help="Overwrite existing matrix entries") + parser.add_argument("--industry", type=str, help="Specific industry name to process") + args = parser.parse_args() + + run_matrix_generation(dry_run=not args.live, force=args.force, specific_industry=args.industry) \ No newline at end of file diff --git a/company-explorer/backend/scripts/import_job_titles.py b/company-explorer/backend/scripts/import_job_titles.py new file mode 100644 index 00000000..b31fe585 --- /dev/null +++ b/company-explorer/backend/scripts/import_job_titles.py @@ -0,0 +1,66 @@ +import sys +import os +import csv +from collections import Counter +import argparse + +# Add the 'backend' directory to the path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from database import SessionLocal, RawJobTitle +from lib.logging_setup import setup_logging +import logging + +setup_logging() +logger = logging.getLogger(__name__) + +def import_job_titles_from_csv(file_path: str): + db = SessionLocal() + try: + logger.info(f"Starting import of job titles from {file_path}") + + # Use Counter to get frequencies directly from the CSV + job_title_counts = Counter() + total_rows = 0 + + with open(file_path, 'r', encoding='utf-8') as f: + reader = csv.reader(f) + # Assuming the CSV contains only job titles, one per row + for row in reader: + if row and row[0].strip(): + title = row[0].strip() + job_title_counts[title] += 1 + total_rows += 1 + + logger.info(f"Read {total_rows} total job title entries. Found {len(job_title_counts)} unique titles.") + + added_count = 0 + updated_count = 0 + + for title, count in job_title_counts.items(): + existing_title = db.query(RawJobTitle).filter(RawJobTitle.title == title).first() + if existing_title: + if existing_title.count != count: + existing_title.count = count + updated_count += 1 + # If it exists and count is the same, do nothing. + else: + new_title = RawJobTitle(title=title, count=count, source="csv_import", is_mapped=False) + db.add(new_title) + added_count += 1 + + db.commit() + logger.info(f"Import complete. Added {added_count} new unique titles, updated {updated_count} existing titles.") + + except Exception as e: + logger.error(f"Error during job title import: {e}", exc_info=True) + db.rollback() + finally: + db.close() + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Import job titles from a CSV file into the RawJobTitle database table.") + parser.add_argument("file_path", type=str, help="Path to the CSV file containing job titles.") + args = parser.parse_args() + + import_job_titles_from_csv(args.file_path) \ No newline at end of file diff --git a/company-explorer/backend/scripts/inspect_sqlite_native.py b/company-explorer/backend/scripts/inspect_sqlite_native.py new file mode 100644 index 00000000..3e28e0cc --- /dev/null +++ b/company-explorer/backend/scripts/inspect_sqlite_native.py @@ -0,0 +1,58 @@ +import sqlite3 +import json + +DB_PATH = "/app/companies_v3_fixed_2.db" + +def inspect(name_part): + try: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + print(f"Searching for '{name_part}' in {DB_PATH}...") + cursor.execute("SELECT id, name, website, industry_ai, calculated_metric_value, standardized_metric_value, ai_opener, ai_opener_secondary FROM companies WHERE name LIKE ?", (f'%{name_part}%',)) + companies = cursor.fetchall() + + if not companies: + print("No hits.") + return + + for c in companies: + cid, name, website, industry, metric, std_metric, opener_primary, opener_secondary = c + print("\n" + "="*40) + print(f"🏢 {name} (ID: {cid})") + print(f" Vertical: {industry}") + print(f" Website: {website}") + print(f" Metric: {metric} (Std: {std_metric})") + print(f" Opener (Primary): {opener_primary}") + print(f" Opener (Secondary): {opener_secondary}") + + # Fetch Enrichment Data + cursor.execute("SELECT source_type, content FROM enrichment_data WHERE company_id = ?", (cid,)) + rows = cursor.fetchall() + print("\n 📚 Enrichment Data:") + for r in rows: + stype, content_raw = r + print(f" - {stype}") + try: + content = json.loads(content_raw) + if stype == "website_scrape": + summary = content.get("summary", "") + raw = content.get("text", "") + print(f" > Summary: {summary[:150]}...") + print(f" > Raw Length: {len(raw)}") + if len(raw) > 500: + print(f" > Raw Snippet: {raw[:300]}...") + elif stype == "wikipedia": + print(f" > URL: {content.get('url')}") + intro = content.get("intro_text", "") or content.get("full_text", "") + print(f" > Intro: {str(intro)[:150]}...") + except: + print(" > (Content not valid JSON)") + + except Exception as e: + print(f"Error: {e}") + finally: + if conn: conn.close() + +if __name__ == "__main__": + inspect("Therme Erding") diff --git a/company-explorer/backend/scripts/inspect_therme.py b/company-explorer/backend/scripts/inspect_therme.py new file mode 100644 index 00000000..407ebc1a --- /dev/null +++ b/company-explorer/backend/scripts/inspect_therme.py @@ -0,0 +1,58 @@ +import sys +import os +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +# Add backend path +sys.path.append(os.path.join(os.path.dirname(__file__), "../../")) + +from backend.database import Company, EnrichmentData +from backend.config import settings + +def inspect_company(company_name_part): + engine = create_engine(settings.DATABASE_URL) + SessionLocal = sessionmaker(bind=engine) + db = SessionLocal() + + try: + print(f"Searching for company containing: '{company_name_part}'...") + companies = db.query(Company).filter(Company.name.ilike(f"%{company_name_part}%")).all() + + if not companies: + print("❌ No company found.") + return + + for company in companies: + print("\n" + "="*60) + print(f"🏢 COMPANY: {company.name} (ID: {company.id})") + print("="*60) + print(f"🌐 Website: {company.website}") + print(f"🏗️ Industry (AI): {company.industry_ai}") + print(f"📊 Metric: {company.calculated_metric_value} {company.calculated_metric_unit} (Std: {company.standardized_metric_value} m²)") + print(f"✅ Status: {company.status}") + + # Enrichment Data + enrichment = db.query(EnrichmentData).filter(EnrichmentData.company_id == company.id).all() + print("\n📚 ENRICHMENT DATA:") + for ed in enrichment: + print(f" 🔹 Type: {ed.source_type} (Locked: {ed.is_locked})") + if ed.source_type == "website_scrape": + content = ed.content + if isinstance(content, dict): + summary = content.get("summary", "No summary") + raw_text = content.get("raw_text", "") + print(f" 📝 Summary: {str(summary)[:200]}...") + print(f" 📄 Raw Text Length: {len(str(raw_text))} chars") + elif ed.source_type == "wikipedia": + content = ed.content + if isinstance(content, dict): + print(f" 🔗 Wiki URL: {content.get('url')}") + print(f" 📄 Content Snippet: {str(content.get('full_text', ''))[:200]}...") + + except Exception as e: + print(f"Error: {e}") + finally: + db.close() + +if __name__ == "__main__": + inspect_company("Therme Erding") \ No newline at end of file diff --git a/company-explorer/backend/scripts/migrate_db.py b/company-explorer/backend/scripts/migrate_db.py index edab195f..87c8fba1 100644 --- a/company-explorer/backend/scripts/migrate_db.py +++ b/company-explorer/backend/scripts/migrate_db.py @@ -89,6 +89,17 @@ def migrate_tables(): """) logger.info("Table 'reported_mistakes' ensured to exist.") + # 4. Update CONTACTS Table (Two-step for SQLite compatibility) + logger.info("Checking 'contacts' table schema for unsubscribe_token...") + contacts_columns = get_table_columns(cursor, "contacts") + + if 'unsubscribe_token' not in contacts_columns: + logger.info("Adding column 'unsubscribe_token' to 'contacts' table...") + cursor.execute("ALTER TABLE contacts ADD COLUMN unsubscribe_token TEXT") + + logger.info("Creating UNIQUE index on 'unsubscribe_token' column...") + cursor.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_contacts_unsubscribe_token ON contacts (unsubscribe_token)") + conn.commit() logger.info("All migrations completed successfully.") diff --git a/company-explorer/backend/scripts/migrate_matrix_campaign.py b/company-explorer/backend/scripts/migrate_matrix_campaign.py new file mode 100644 index 00000000..354b1b7f --- /dev/null +++ b/company-explorer/backend/scripts/migrate_matrix_campaign.py @@ -0,0 +1,43 @@ +import sys +import os + +# Pfade so setzen, dass das Backend gefunden wird +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) + +from backend.database import SessionLocal, engine +from sqlalchemy import text + +def migrate(): + print("🚀 Starting Migration: Adding 'campaign_tag' to MarketingMatrix...") + + conn = engine.connect() + + try: + # 1. Prüfen, ob Spalte schon existiert + # SQLite Pragma: table_info(marketing_matrix) + result = conn.execute(text("PRAGMA table_info(marketing_matrix)")).fetchall() + columns = [row[1] for row in result] + + if "campaign_tag" in columns: + print("✅ Column 'campaign_tag' already exists. Skipping.") + return + + # 2. Spalte hinzufügen (SQLite supports simple ADD COLUMN) + print("Adding column 'campaign_tag' (DEFAULT 'standard')...") + conn.execute(text("ALTER TABLE marketing_matrix ADD COLUMN campaign_tag VARCHAR DEFAULT 'standard'")) + + # 3. Index erstellen (Optional, aber gut für Performance) + print("Creating index on 'campaign_tag'...") + conn.execute(text("CREATE INDEX ix_marketing_matrix_campaign_tag ON marketing_matrix (campaign_tag)")) + + conn.commit() + print("✅ Migration successful!") + + except Exception as e: + print(f"❌ Migration failed: {e}") + conn.rollback() + finally: + conn.close() + +if __name__ == "__main__": + migrate() diff --git a/company-explorer/backend/scripts/migrate_opener.py b/company-explorer/backend/scripts/migrate_opener.py new file mode 100644 index 00000000..68542cbd --- /dev/null +++ b/company-explorer/backend/scripts/migrate_opener.py @@ -0,0 +1,31 @@ +from sqlalchemy import create_engine, text +import sys +import os + +# Add backend path +sys.path.append(os.path.join(os.path.dirname(__file__), "../../")) +from backend.config import settings + +def migrate(): + engine = create_engine(settings.DATABASE_URL) + with engine.connect() as conn: + try: + # Check if column exists + print("Checking schema...") + # SQLite specific pragma + result = conn.execute(text("PRAGMA table_info(companies)")) + columns = [row[1] for row in result.fetchall()] + + if "ai_opener" in columns: + print("Column 'ai_opener' already exists. Skipping.") + else: + print("Adding column 'ai_opener' to 'companies' table...") + conn.execute(text("ALTER TABLE companies ADD COLUMN ai_opener TEXT")) + conn.commit() + print("✅ Migration successful.") + + except Exception as e: + print(f"❌ Migration failed: {e}") + +if __name__ == "__main__": + migrate() diff --git a/company-explorer/backend/scripts/notion_maintenance/analyze_verticals_full.py b/company-explorer/backend/scripts/notion_maintenance/analyze_verticals_full.py new file mode 100644 index 00000000..10708d3f --- /dev/null +++ b/company-explorer/backend/scripts/notion_maintenance/analyze_verticals_full.py @@ -0,0 +1,112 @@ +import os +import requests +import json +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +NOTION_API_KEY = os.getenv("NOTION_API_KEY") +NOTION_DB_VERTICALS = "2ec88f4285448014ab38ea664b4c2b81" +NOTION_DB_PRODUCTS = "2ec88f42854480f0b154f7a07342eb58" + +if not NOTION_API_KEY: + print("Error: NOTION_API_KEY not found.") + exit(1) + +headers = { + "Authorization": f"Bearer {NOTION_API_KEY}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" +} + +def fetch_all_pages(db_id): + pages = [] + has_more = True + start_cursor = None + + while has_more: + url = f"https://api.notion.com/v1/databases/{db_id}/query" + payload = {"page_size": 100} + if start_cursor: + payload["start_cursor"] = start_cursor + + response = requests.post(url, headers=headers, json=payload) + if response.status_code != 200: + print(f"Error fetching DB {db_id}: {response.status_code} - {response.text}") + break + + data = response.json() + pages.extend(data.get("results", [])) + has_more = data.get("has_more", False) + start_cursor = data.get("next_cursor") + + return pages + +def get_property_text(page, prop_name): + props = page.get("properties", {}) + prop = props.get(prop_name) + if not prop: + return "" + + prop_type = prop.get("type") + + if prop_type == "title": + return "".join([t["plain_text"] for t in prop.get("title", [])]) + elif prop_type == "rich_text": + return "".join([t["plain_text"] for t in prop.get("rich_text", [])]) + elif prop_type == "select": + select = prop.get("select") + return select.get("name") if select else "" + elif prop_type == "multi_select": + return ", ".join([s["name"] for s in prop.get("multi_select", [])]) + elif prop_type == "relation": + return [r["id"] for r in prop.get("relation", [])] + else: + return f"[Type: {prop_type}]" + +def main(): + print("--- 1. Fetching Product Categories ---") + product_pages = fetch_all_pages(NOTION_DB_PRODUCTS) + product_map = {} + for p in product_pages: + p_id = p["id"] + # Product Category name is likely the title property + # Let's find the title property key dynamically + title_key = next((k for k, v in p["properties"].items() if v["id"] == "title"), "Name") + name = get_property_text(p, title_key) + product_map[p_id] = name + # print(f"Product: {name} ({p_id})") + + print(f"Loaded {len(product_map)} products.") + + print("\n--- 2. Fetching Verticals ---") + vertical_pages = fetch_all_pages(NOTION_DB_VERTICALS) + + print("\n--- 3. Analysis ---") + for v in vertical_pages: + # Determine Title Key (Vertical Name) + title_key = next((k for k, v in v["properties"].items() if v["id"] == "title"), "Vertical") + vertical_name = get_property_text(v, title_key) + + # Primary Product + pp_ids = get_property_text(v, "Primary Product Category") + pp_names = [product_map.get(pid, f"Unknown ({pid})") for pid in pp_ids] if isinstance(pp_ids, list) else [] + + # Secondary Product + sp_ids = get_property_text(v, "Secondary Product") + sp_names = [product_map.get(pid, f"Unknown ({pid})") for pid in sp_ids] if isinstance(sp_ids, list) else [] + + # Pains & Gains + pains = get_property_text(v, "Pains") + gains = get_property_text(v, "Gains") + + print(f"\n### {vertical_name}") + print(f"**Primary Product:** {', '.join(pp_names)}") + print(f"**Secondary Product:** {', '.join(sp_names)}") + print(f"**Pains:**\n{pains.strip()}") + print(f"**Gains:**\n{gains.strip()}") + print("-" * 40) + +if __name__ == "__main__": + main() diff --git a/company-explorer/backend/scripts/notion_maintenance/check_notion_verticals.py b/company-explorer/backend/scripts/notion_maintenance/check_notion_verticals.py new file mode 100644 index 00000000..522ee598 --- /dev/null +++ b/company-explorer/backend/scripts/notion_maintenance/check_notion_verticals.py @@ -0,0 +1,117 @@ +import os +import requests +import json +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +NOTION_API_KEY = os.getenv("NOTION_API_KEY") +NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81" # ID from the user's link + +if not NOTION_API_KEY: + print("Error: NOTION_API_KEY not found in environment.") + exit(1) + +headers = { + "Authorization": f"Bearer {NOTION_API_KEY}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" +} + +def get_vertical_data(vertical_name): + url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query" + payload = { + "filter": { + "property": "Vertical", + "title": { + "contains": vertical_name + } + } + } + + response = requests.post(url, headers=headers, json=payload) + + if response.status_code != 200: + print(f"Error fetching data for '{vertical_name}': {response.status_code} - {response.text}") + return None + + results = response.json().get("results", []) + if not results: + print(f"No entry found for vertical '{vertical_name}'") + return None + + # Assuming the first result is the correct one + page = results[0] + props = page["properties"] + + # Extract Pains + pains_prop = props.get("Pains", {}).get("rich_text", []) + pains = pains_prop[0]["plain_text"] if pains_prop else "N/A" + + # Extract Gains + gains_prop = props.get("Gains", {}).get("rich_text", []) + gains = gains_prop[0]["plain_text"] if gains_prop else "N/A" + + # Extract Ops Focus (Checkbox) if available + # The property name might be "Ops. Focus: Secondary" based on user description + # Let's check keys to be sure, but user mentioned "Ops. Focus: Secondary" + # Actually, let's just dump the keys if needed, but for now try to guess + ops_focus = "Unknown" + if "Ops. Focus: Secondary" in props: + ops_focus = props["Ops. Focus: Secondary"].get("checkbox", False) + elif "Ops Focus" in props: # Fallback guess + ops_focus = props["Ops Focus"].get("checkbox", False) + + # Extract Product Categories + primary_product = "N/A" + secondary_product = "N/A" + + # Assuming these are Select or Multi-select fields, or Relations. + # User mentioned "Primary Product Category" and "Secondary Product Category". + if "Primary Product Category" in props: + pp_data = props["Primary Product Category"].get("select") or props["Primary Product Category"].get("multi_select") + if pp_data: + if isinstance(pp_data, list): + primary_product = ", ".join([item["name"] for item in pp_data]) + else: + primary_product = pp_data["name"] + + if "Secondary Product Category" in props: + sp_data = props["Secondary Product Category"].get("select") or props["Secondary Product Category"].get("multi_select") + if sp_data: + if isinstance(sp_data, list): + secondary_product = ", ".join([item["name"] for item in sp_data]) + else: + secondary_product = sp_data["name"] + + return { + "name": vertical_name, + "pains": pains, + "gains": gains, + "ops_focus_secondary": ops_focus, + "primary_product": primary_product, + "secondary_product": secondary_product + } + +verticals_to_check = [ + "Krankenhaus", + "Pflege", # Might be "Altenheim" or similar + "Hotel", + "Industrie", # Might be "Manufacturing" + "Logistik", + "Einzelhandel", + "Facility Management" +] + +print("-" * 60) +for v in verticals_to_check: + data = get_vertical_data(v) + if data: + print(f"VERTICAL: {data['name']}") + print(f" Primary Product: {data['primary_product']}") + print(f" Secondary Product: {data['secondary_product']}") + print(f" Ops. Focus Secondary: {data['ops_focus_secondary']}") + print(f" PAINS: {data['pains']}") + print(f" GAINS: {data['gains']}") + print("-" * 60) diff --git a/company-explorer/backend/scripts/notion_maintenance/check_relations.py b/company-explorer/backend/scripts/notion_maintenance/check_relations.py new file mode 100644 index 00000000..30151bad --- /dev/null +++ b/company-explorer/backend/scripts/notion_maintenance/check_relations.py @@ -0,0 +1,90 @@ +import os +import requests +import json +from dotenv import load_dotenv + +load_dotenv() + +NOTION_API_KEY = os.getenv("NOTION_API_KEY") +NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81" # Verticals DB +PRODUCT_DB_ID = "2ec88f42854480f0b154f7a07342eb58" # Product Categories DB (from user link) + +headers = { + "Authorization": f"Bearer {NOTION_API_KEY}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" +} + +# 1. Fetch Product Map (ID -> Name) +product_map = {} +def fetch_products(): + url = f"https://api.notion.com/v1/databases/{PRODUCT_DB_ID}/query" + response = requests.post(url, headers=headers, json={"page_size": 100}) + if response.status_code == 200: + results = response.json().get("results", []) + for p in results: + p_id = p["id"] + # Name property might be "Name" or "Product Category" + props = p["properties"] + name = "Unknown" + if "Name" in props: + name = props["Name"]["title"][0]["plain_text"] if props["Name"]["title"] else "N/A" + elif "Product Category" in props: + name = props["Product Category"]["title"][0]["plain_text"] if props["Product Category"]["title"] else "N/A" + + product_map[p_id] = name + # Also map the page ID itself if used in relations + + else: + print(f"Error fetching products: {response.status_code}") + +# 2. Check Verticals with Relation Resolution +def check_vertical_relations(search_term): + url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query" + payload = { + "filter": { + "property": "Vertical", + "title": { + "contains": search_term + } + } + } + resp = requests.post(url, headers=headers, json=payload) + if resp.status_code == 200: + results = resp.json().get("results", []) + if not results: + print(f"❌ No vertical found for '{search_term}'") + return + + for page in results: + props = page["properties"] + title = props["Vertical"]["title"][0]["plain_text"] + + # Resolve Primary + pp_ids = [r["id"] for r in props.get("Primary Product Category", {}).get("relation", [])] + pp_names = [product_map.get(pid, pid) for pid in pp_ids] + + # Resolve Secondary + sp_ids = [r["id"] for r in props.get("Secondary Product", {}).get("relation", [])] + sp_names = [product_map.get(pid, pid) for pid in sp_ids] + + print(f"\n🔹 VERTICAL: {title}") + print(f" Primary Product (Rel): {', '.join(pp_names)}") + print(f" Secondary Product (Rel): {', '.join(sp_names)}") + + # Pains/Gains short check + pains = props.get("Pains", {}).get("rich_text", []) + print(f" Pains Length: {len(pains[0]['plain_text']) if pains else 0} chars") + + else: + print(f"Error fetching vertical: {resp.status_code}") + +# Run +print("Fetching Product Map...") +fetch_products() +print(f"Loaded {len(product_map)} products.") + +print("\nChecking Verticals...") +targets = ["Hospital", "Hotel", "Logistics", "Manufacturing", "Retail", "Reinigungs", "Dienstleister", "Facility"] +for t in targets: + check_vertical_relations(t) diff --git a/company-explorer/backend/scripts/notion_maintenance/check_specific_verticals.py b/company-explorer/backend/scripts/notion_maintenance/check_specific_verticals.py new file mode 100644 index 00000000..a7635882 --- /dev/null +++ b/company-explorer/backend/scripts/notion_maintenance/check_specific_verticals.py @@ -0,0 +1,87 @@ +import os +import requests +import json +from dotenv import load_dotenv + +load_dotenv() + +NOTION_API_KEY = os.getenv("NOTION_API_KEY") +NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81" + +if not NOTION_API_KEY: + print("Error: NOTION_API_KEY not found.") + exit(1) + +headers = { + "Authorization": f"Bearer {NOTION_API_KEY}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" +} + +def get_vertical_details(vertical_name_contains): + url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query" + payload = { + "filter": { + "property": "Vertical", + "title": { + "contains": vertical_name_contains + } + } + } + + response = requests.post(url, headers=headers, json=payload) + if response.status_code != 200: + print(f"Error: {response.status_code}") + return + + results = response.json().get("results", []) + if not results: + print(f"❌ No entry found containing '{vertical_name_contains}'") + return + + for page in results: + props = page["properties"] + + # safely extract title + title_list = props.get("Vertical", {}).get("title", []) + title = title_list[0]["plain_text"] if title_list else "Unknown Title" + + # Pains + pains_list = props.get("Pains", {}).get("rich_text", []) + pains = pains_list[0]["plain_text"] if pains_list else "N/A" + + # Gains + gains_list = props.get("Gains", {}).get("rich_text", []) + gains = gains_list[0]["plain_text"] if gains_list else "N/A" + + # Ops Focus + ops_focus = props.get("Ops Focus: Secondary", {}).get("checkbox", False) + + # Products + # Primary is select + pp_select = props.get("Primary Product Category", {}).get("select") + pp = pp_select["name"] if pp_select else "N/A" + + # Secondary is select + sp_select = props.get("Secondary Product", {}).get("select") + sp = sp_select["name"] if sp_select else "N/A" + + print(f"\n🔹 VERTICAL: {title}") + print(f" Primary: {pp}") + print(f" Secondary: {sp}") + print(f" Ops Focus Secondary? {'✅ YES' if ops_focus else '❌ NO'}") + print(f" PAINS:\n {pains}") + print(f" GAINS:\n {gains}") + print("-" * 40) + +targets = [ + "Hospital", + "Hotel", + "Logistics", + "Manufacturing", + "Retail", + "Facility Management" +] + +for t in targets: + get_vertical_details(t) diff --git a/company-explorer/backend/scripts/notion_maintenance/inspect_notion_properties.py b/company-explorer/backend/scripts/notion_maintenance/inspect_notion_properties.py new file mode 100644 index 00000000..581fd6c1 --- /dev/null +++ b/company-explorer/backend/scripts/notion_maintenance/inspect_notion_properties.py @@ -0,0 +1,38 @@ +import os +import requests +import json +from dotenv import load_dotenv + +load_dotenv() + +# Check for API Key +NOTION_API_KEY = os.getenv("NOTION_API_KEY") +if not NOTION_API_KEY: + try: + with open("/app/n8n_api_Token_git.txt", "r") as f: + content = f.read() + if "secret_" in content: + NOTION_API_KEY = content.strip().split('\n')[0] + except: + pass + +if not NOTION_API_KEY: + print("Error: NOTION_API_KEY not found.") + exit(1) + +NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81" +headers = {"Authorization": f"Bearer {NOTION_API_KEY}", "Notion-Version": "2022-06-28", "Content-Type": "application/json"} + +def list_db_properties(): + url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}" + resp = requests.get(url, headers=headers) + if resp.status_code == 200: + props = resp.json().get("properties", {}) + print("Database Properties:") + for name, data in props.items(): + print(f"- {name} (Type: {data['type']})") + else: + print(f"Error getting DB: {resp.text}") + +if __name__ == "__main__": + list_db_properties() diff --git a/company-explorer/backend/scripts/notion_maintenance/list_notion_structure.py b/company-explorer/backend/scripts/notion_maintenance/list_notion_structure.py new file mode 100644 index 00000000..3f0d9c00 --- /dev/null +++ b/company-explorer/backend/scripts/notion_maintenance/list_notion_structure.py @@ -0,0 +1,66 @@ +import os +import requests +import json +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +NOTION_API_KEY = os.getenv("NOTION_API_KEY") +NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81" + +if not NOTION_API_KEY: + print("Error: NOTION_API_KEY not found in environment.") + exit(1) + +headers = { + "Authorization": f"Bearer {NOTION_API_KEY}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" +} + +def list_pages_and_keys(): + url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query" + payload = { + "page_size": 10 # Just list a few to see structure + } + + response = requests.post(url, headers=headers, json=payload) + + if response.status_code != 200: + print(f"Error fetching data: {response.status_code} - {response.text}") + return + + results = response.json().get("results", []) + + if not results: + print("No pages found.") + return + + print(f"Found {len(results)} pages.") + + # Print keys from the first page + first_page = results[0] + props = first_page["properties"] + print("\n--- Property Keys Found ---") + for key in props.keys(): + print(f"- {key}") + + print("\n--- Page Titles (Verticals) ---") + for page in results: + title_prop = page["properties"].get("Vertical", {}).get("title", []) # Assuming title prop is named "Vertical" based on user input + if not title_prop: + # Try finding the title property dynamically if "Vertical" is wrong + for k, v in page["properties"].items(): + if v["id"] == "title": + title_prop = v["title"] + break + + if title_prop: + title = title_prop[0]["plain_text"] + print(f"- {title}") + else: + print("- (No Title)") + +if __name__ == "__main__": + list_pages_and_keys() diff --git a/company-explorer/backend/scripts/notion_maintenance/update_notion_full.py b/company-explorer/backend/scripts/notion_maintenance/update_notion_full.py new file mode 100644 index 00000000..d7fbe0f5 --- /dev/null +++ b/company-explorer/backend/scripts/notion_maintenance/update_notion_full.py @@ -0,0 +1,89 @@ +import os +import requests +import json +from dotenv import load_dotenv + +load_dotenv() + +NOTION_API_KEY = os.getenv("NOTION_API_KEY") +NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81" + +if not NOTION_API_KEY: + print("Error: NOTION_API_KEY not found.") + exit(1) + +headers = { + "Authorization": f"Bearer {NOTION_API_KEY}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" +} + +# COMPLETE LIST OF UPDATES +updates = { + "Infrastructure - Transport": { # Airports, Stations + "Pains": "Sicherheitsbereiche erfordern personalintensives Screening von externen Reinigungskräften. Verschmutzte Böden (Winter/Salz) erhöhen das Rutschrisiko für Passagiere und Klagerisiken.", + "Gains": "Autonome Reinigung innerhalb der Sicherheitszonen ohne externe Personalwechsel. Permanente Trocknung von Nässe (Schneematsch) in Eingangsbereichen." + }, + "Leisure - Indoor Active": { # Bowling, Cinema, Gym + "Pains": "Personal ist rar und teuer, Gäste erwarten aber Service am Platz. Reinigung im laufenden Betrieb stört den Erlebnischarakter.", + "Gains": "Service-Roboter als Event-Faktor und Entlastung: Getränke kommen zum Gast, Personal bleibt an der Bar/Theke. Konstante Sauberkeit auch bei hoher Frequenz." + }, + "Leisure - Outdoor Park": { # Zoos, Theme Parks + "Pains": "Enorme Flächenleistung (Wege) erfordert viele Arbeitskräfte für die Grobschmutzbeseitigung (Laub, Müll). Sichtbare Reinigungstrupps stören die Immersion der Gäste.", + "Gains": "Autonome Großflächenreinigung (Kehren) in den frühen Morgenstunden vor Parköffnung. Erhalt der 'heilen Welt' (Immersion) für Besucher." + }, + "Leisure - Wet & Spa": { # Pools, Thermen + "Pains": "Hohes Unfallrisiko durch Nässe auf Fliesen (Rutschgefahr). Hoher Aufwand für permanente Desinfektion und Trocknung im laufenden Betrieb bindet Aufsichtspersonal.", + "Gains": "Permanente Trocknung und Desinfektion kritischer Barfußbereiche. Reduktion der Rutschgefahr und Haftungsrisiken. Entlastung der Bademeister (Fokus auf Aufsicht)." + }, + "Retail - Shopping Center": { # Malls + "Pains": "Food-Court ist der Schmutz-Hotspot: Verschüttete Getränke und Essensreste wirken unhygienisch und binden Personal dauerhaft. Dreckige Böden senken die Verweildauer.", + "Gains": "Sofortige Beseitigung von Malheuren im Food-Court. Steigerung der Aufenthaltsqualität und Verweildauer der Kunden durch sichtbare Sauberkeit." + }, + "Retail - Non-Food": { # DIY, Furniture + "Pains": "Riesige Gangflächen verstauben schnell, Personal ist knapp und soll beraten, nicht kehren. Verschmutzte Böden wirken im Premium-Segment (Möbel) wertmindernd.", + "Gains": "Staubfreie Umgebung für angenehmes Einkaufsklima. Roboter reinigen autonom große Flächen, während Mitarbeiter für Kundenberatung verfügbar sind." + }, + "Infrastructure - Public": { # Fairs, Schools + "Pains": "Extrem kurze Turnaround-Zeiten zwischen Messetagen oder Events. Hohe Nachtzuschläge für die Endreinigung der Hallengänge oder Klassenzimmer.", + "Gains": "Automatisierte Nachtreinigung der Gänge/Flure stellt die Optik für den nächsten Morgen sicher. Kalkulierbare Kosten ohne Nachtzuschlag." + }, + "Hospitality - Gastronomy": { # Restaurants + "Pains": "Servicepersonal verbringt Zeit auf Laufwegen statt am Gast ('Teller-Taxi'). Personalmangel führt zu langen Wartezeiten und Umsatzverlust.", + "Gains": "Servicekräfte werden von Laufwegen befreit und haben Zeit für aktive Beratung und Verkauf (Upselling). Steigerung der Tischumschlagshäufigkeit." + } +} + +def update_vertical(vertical_name, new_data): + url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query" + payload = { + "filter": { + "property": "Vertical", + "title": { + "contains": vertical_name + } + } + } + resp = requests.post(url, headers=headers, json=payload) + if resp.status_code != 200: return + + results = resp.json().get("results", []) + if not results: + print(f"Skipping {vertical_name} (Not found)") + return + + page_id = results[0]["id"] + update_url = f"https://api.notion.com/v1/pages/{page_id}" + update_payload = { + "properties": { + "Pains": {"rich_text": [{"text": {"content": new_data["Pains"]}}]}, + "Gains": {"rich_text": [{"text": {"content": new_data["Gains"]}}]} + } + } + requests.patch(update_url, headers=headers, json=update_payload) + print(f"✅ Updated {vertical_name}") + +print("Starting FULL Notion Update...") +for v_name, data in updates.items(): + update_vertical(v_name, data) +print("Done.") diff --git a/company-explorer/backend/scripts/notion_maintenance/update_notion_pains_gains.py b/company-explorer/backend/scripts/notion_maintenance/update_notion_pains_gains.py new file mode 100644 index 00000000..6af2a4a7 --- /dev/null +++ b/company-explorer/backend/scripts/notion_maintenance/update_notion_pains_gains.py @@ -0,0 +1,94 @@ +import os +import requests +import json +from dotenv import load_dotenv + +load_dotenv() + +NOTION_API_KEY = os.getenv("NOTION_API_KEY") +NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81" + +if not NOTION_API_KEY: + print("Error: NOTION_API_KEY not found.") + exit(1) + +headers = { + "Authorization": f"Bearer {NOTION_API_KEY}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" +} + +# Define the updates with "Sharp" Pains/Gains +updates = { + "Healthcare - Hospital": { + "Pains": "Fachpflegekräfte sind bis zu 30% der Schichtzeit mit logistischen Routinetätigkeiten (Wäsche, Essen, Laborproben) gebunden ('Hände weg vom Bett'). Steigende Hygienerisiken bei gleichzeitigem Personalmangel im Reinigungsteam führen zu lückenhafter Dokumentation und Gefährdung der RKI-Konformität.", + "Gains": "Rückgewinnung von ca. 2,5h Fachkraft-Kapazität pro Schicht durch automatisierte Stationslogistik. Validierbare, RKI-konforme Reinigungsqualität rund um die Uhr, unabhängig vom Krankenstand des Reinigungsteams." + }, + "Hospitality - Hotel": { + "Pains": "Enorme Fluktuation im Housekeeping gefährdet die pünktliche Zimmer-Freigabe (Check-in 15:00 Uhr). Hohe Nachtzuschläge oder fehlendes Personal verhindern, dass die Lobby und Konferenzbereiche morgens um 06:00 Uhr perfekt glänzen.", + "Gains": "Lautlose Nachtreinigung der Lobby und Flure ohne Personalzuschläge. Servicekräfte im Restaurant werden von Laufwegen ('Teller-Taxi') befreit und haben Zeit für aktives Upselling am Gast." + }, + "Logistics - Warehouse": { + "Pains": "Verschmutzte Fahrwege durch Palettenabrieb und Staub gefährden die Sensorik von FTS (Fahrerlosen Transportsystemen) und erhöhen das Unfallrisiko für Flurförderzeuge. Manuelle Reinigung stört den 24/7-Betrieb und bindet Fachpersonal.", + "Gains": "Permanente Staubreduktion im laufenden Betrieb schützt empfindliche Anlagentechnik (Lichtschranken). Saubere Hallen als Visitenkarte und Sicherheitsfaktor (Rutschgefahr), ohne operative Unterbrechungen." + }, + "Industry - Manufacturing": { + "Pains": "Hochbezahlte Facharbeiter unterbrechen die Wertschöpfung für unproduktive Such- und Holzeiten von Material (C-Teile). Intransparente Materialflüsse an der Linie führen zu Mikrostillständen und gefährden die Taktzeit.", + "Gains": "Just-in-Time Materialversorgung direkt an die Linie. Fachkräfte bleiben an der Maschine. Stabilisierung der Taktzeiten und OEE durch automatisierten Nachschub." + }, + "Reinigungsdienstleister": { # Facility Management + "Pains": "Margendruck durch steigende Tariflöhne bei gleichzeitigem Preisdiktat der Auftraggeber. Hohe Fluktuation (>30%) führt zu ständiger Rekrutierung ('No-Show'-Quote), was Objektleiter bindet und die Qualitätskontrolle vernachlässigt.", + "Gains": "Kalkulationssicherheit durch Fixkosten statt variabler Personalkosten. Garantierte Reinigungsleistung in Objekten unabhängig vom Personalstand. Innovationsträger für Ausschreibungen." + }, + "Retail - Food": { # Supermarkets + "Pains": "Reinigungskosten steigen linear zur Fläche, während Kundenfrequenz schwankt. Sichtbare Reinigungsmaschinen blockieren tagsüber Kundenwege ('Störfaktor'). Abends/Nachts schwer Personal zu finden.", + "Gains": "Unsichtbare Reinigung: Roboter fahren in Randzeiten oder weichen Kunden dynamisch aus. Konstantes Sauberkeits-Level ('Lobby-Effekt') steigert Verweildauer." + } +} + +def update_vertical(vertical_name, new_data): + # 1. Find Page ID + url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query" + payload = { + "filter": { + "property": "Vertical", + "title": { + "contains": vertical_name + } + } + } + resp = requests.post(url, headers=headers, json=payload) + if resp.status_code != 200: + print(f"Error searching {vertical_name}: {resp.status_code}") + return + + results = resp.json().get("results", []) + if not results: + print(f"Skipping {vertical_name} (Not found)") + return + + page_id = results[0]["id"] + + # 2. Update Page + update_url = f"https://api.notion.com/v1/pages/{page_id}" + update_payload = { + "properties": { + "Pains": { + "rich_text": [{"text": {"content": new_data["Pains"]}}] + }, + "Gains": { + "rich_text": [{"text": {"content": new_data["Gains"]}}] + } + } + } + + upd_resp = requests.patch(update_url, headers=headers, json=update_payload) + if upd_resp.status_code == 200: + print(f"✅ Updated {vertical_name}") + else: + print(f"❌ Failed to update {vertical_name}: {upd_resp.text}") + +print("Starting Notion Update...") +for v_name, data in updates.items(): + update_vertical(v_name, data) +print("Done.") diff --git a/company-explorer/backend/scripts/notion_maintenance/update_verticals_phase1.py b/company-explorer/backend/scripts/notion_maintenance/update_verticals_phase1.py new file mode 100644 index 00000000..78f9575e --- /dev/null +++ b/company-explorer/backend/scripts/notion_maintenance/update_verticals_phase1.py @@ -0,0 +1,194 @@ +import os +import requests +import json +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +NOTION_API_KEY = os.getenv("NOTION_API_KEY") +NOTION_DB_VERTICALS = "2ec88f4285448014ab38ea664b4c2b81" + +if not NOTION_API_KEY: + print("Error: NOTION_API_KEY not found.") + exit(1) + +headers = { + "Authorization": f"Bearer {NOTION_API_KEY}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" +} + +# The approved changes from ANALYSIS_AND_PROPOSAL.md +UPDATES = { + "Automotive - Dealer": { + "Pains": """[Primary Product: Security] +- Teile-Diebstahl: Organisierte Banden demontieren nachts Katalysatoren und Räder – enormer Schaden und Versicherungsstress. +- Vandalismus: Zerkratzte Neuwagen auf dem Außenhof mindern den Verkaufswert drastisch. +- Personalkosten: Lückenlose menschliche Nachtbewachung ist für viele Standorte wirtschaftlich kaum darstellbar. + +[Secondary Product: Cleaning Outdoor] +- Image-Verlust: Ein verschmutzter Außenbereich (Laub, Müll) passt nicht zum Premium-Anspruch der ausgestellten Fahrzeuge. +- Manueller Aufwand: Verkaufspersonal oder teure Hausmeisterdienste binden Zeit mit unproduktivem Fegen.""", + "Gains": """[Primary Product: Security] +- Abschreckung & Intervention: Permanente Roboter-Präsenz wirkt präventiv; bei Alarm schaltet sich sofort eine Leitstelle auf. +- Asset-Schutz: Reduktion von Versicherungsschäden und Selbstbehalten durch lückenlose Dokumentation. + +[Secondary Product: Cleaning Outdoor] +- Premium-Präsentation: Der Hof ist bereits morgens bei Kundenöffnung makellos sauber. +- Automatisierung: Täglich gereinigte Flächen ohne manuellen Eingriff.""" + }, + "Industry - Manufacturing": { + "Pains": """[Primary Product: Cleaning Indoor] +- Prozess-Sicherheit: Staub und Abrieb auf Fahrwegen gefährden empfindliche Sensorik (z.B. von FTS) und die Produktqualität. +- Arbeitssicherheit: Rutschgefahr durch feine Staubschichten oder ausgelaufene (nicht-chemische) Flüssigkeiten erhöht das Unfallrisiko. +- Ressourcen-Verschwendung: Hochbezahlte Fachkräfte müssen Maschinen stoppen, um ihr Umfeld zu reinigen. + +[Secondary Product: Transport] +- Intransparenz & Suchzeiten: Facharbeiter unterbrechen die Wertschöpfung für unproduktive Materialbeschaffung ("C-Teile holen"). +- Mikrostillstände: Fehlendes Material an der Linie stoppt den Takt.""", + "Gains": """[Primary Product: Cleaning Indoor] +- Konstante Bodenqualität: Definierte Sauberkeitsstandards (Audit-Ready) rund um die Uhr. +- Unfallschutz: Reduktion von Arbeitsunfällen durch rutschfreie Verkehrswege. + +[Secondary Product: Transport] +- Just-in-Time Logistik: Automatisierter Nachschub hält die Fachkraft wertschöpfend an der Maschine. +- Fluss-Optimierung: Stabilisierung der Taktzeiten und OEE durch verlässliche Materialflüsse.""" + }, + "Healthcare - Hospital": { + "Pains": """[Primary Product: Cleaning Indoor] +- Hygienerisiko & Kreuzkontamination: Manuelle Reinigung ist oft fehleranfällig und variiert stark in der Qualität (Gefahr für Patienten). +- Dokumentationspflicht: Der Nachweis RKI-konformer Reinigung bindet wertvolle Zeit und ist bei Personalmangel lückenhaft. +- Personalnot: Fehlende Reinigungskräfte führen zu gesperrten Bereichen oder sinkendem Hygienelevel. + +[Secondary Product: Service] +- Berufsfremde Tätigkeiten: Pflegekräfte verbringen bis zu 30% der Schichtzeit mit Hol- und Bringdiensten (Essen, Wäsche, Labor). +- Physische Überlastung: Lange Laufwege in großen Kliniken erhöhen die Erschöpfung des Fachpersonals.""", + "Gains": """[Primary Product: Cleaning Indoor] +- Validierbare Hygiene: Robotergarantierte, protokollierte Desinfektionsleistung – audit-sicher auf Knopfdruck. +- 24/7 Verfügbarkeit: Konstantes Hygienelevel auch nachts und am Wochenende, unabhängig vom Dienstplan. + +[Secondary Product: Service] +- Zeit für Patienten: Rückgewinnung von ca. 2,5 Stunden Fachkraft-Kapazität pro Schicht für die Pflege. +- Mitarbeiterzufriedenheit: Reduktion der Laufwege ("Schrittzähler") entlastet das Team spürbar.""" + }, + "Logistics - Warehouse": { + "Pains": """[Primary Product: Cleaning (Sweeper/Dry)] +- Grobschmutz & Palettenreste: Holzspäne und Verpackungsreste gefährden Reifen von Flurförderzeugen und blockieren Lichtschranken. +- Staubbelastung: Aufgewirbelter Staub legt sich auf Waren und Verpackungen (Reklamationsgrund) und schadet der Gesundheit. +- Manuelle Bindung: Mitarbeiter müssen große Flächen manuell kehren, statt zu kommissionieren. + +[Secondary Product: Cleaning (Wet)] +- Hartnäckige Verschmutzungen: Eingefahrene Spuren, die durch reines Kehren nicht lösbar sind.""", + "Gains": """[Primary Product: Cleaning (Sweeper/Dry)] +- Anlagenschutz: Sauberer Boden verhindert Störungen an Fördertechnik und Sensoren durch Staub/Teile. +- Staubfreie Ware: Produkte verlassen das Lager in sauberem Zustand (Qualitätsanspruch). + +[Secondary Product: Cleaning (Wet)] +- Grundsauberkeit: Gelegentliche Nassreinigung für Tiefenhygiene in Fahrgassen.""" + }, + "Retail - Food": { + "Pains": """[Primary Product: Cleaning Indoor] +- "Malheur-Management": Zerbrochene Gläser oder ausgelaufene Flüssigkeiten (Haverien) bilden sofortige Rutschfallen und binden Personal. +- Optischer Eindruck: Grauschleier und verschmutzte Böden senken das Frische-Empfinden der Kunden massiv. +- Personal-Engpass: Marktpersonal soll Regale füllen und kassieren, nicht mit der Scheuersaugmaschine fahren. + +[Secondary Product: Service] +- Fehlende Beratung: Kunden finden Produkte nicht und brechen den Kauf ab, da kein Personal greifbar ist.""", + "Gains": """[Primary Product: Cleaning Indoor] +- Sofortige Sicherheit: Roboter beseitigt Rutschgefahren autonom und schnell. +- Frische-Optik: Permanent glänzende Böden ("Lobby-Effekt") unterstreichen die Qualität der Lebensmittel. + +[Secondary Product: Service] +- Umsatz-Boost: Roboter führt Kunden direkt zum gesuchten Produkt oder bewirbt Aktionen aktiv am POS.""" + }, + "Hospitality - Gastronomy": { + "Pains": """[Primary Product: Cleaning Indoor] +- Klebrige Böden: Verschüttete Getränke und Speisereste wirken unhygienisch und stören das Ambiente. +- Randzeiten-Problem: Nach Schließung ist es schwer, Personal für die Grundreinigung zu finden (Nachtzuschläge). + +[Secondary Product: Service] +- "Teller-Taxi": Servicekräfte verbringen 80% der Zeit mit Laufen (Küche <-> Gast) statt mit Verkaufen/Betreuung. +- Personalmangel: Zu wenig Kellner führen zu langen Wartezeiten, kalten Speisen und genervten Gästen.""", + "Gains": """[Primary Product: Cleaning Indoor] +- Makelloses Ambiente: Sauberer Boden als Visitenkarte des Restaurants. +- Zuverlässigkeit: Die Grundreinigung findet jede Nacht garantiert statt. + +[Secondary Product: Service] +- Mehr Umsatz am Gast: Servicekraft hat Zeit für Empfehlungen (Wein, Dessert) und Upselling. +- Entlastung: Roboter übernimmt das schwere Tragen (Tabletts), Personal bleibt im Gastraum präsent.""" + }, + "Leisure - Outdoor Park": { + "Pains": """[Primary Product: Cleaning Outdoor] +- Immersion-Breaker: Müll und Laub auf den Wegen stören die perfekte Illusion ("Heile Welt") des Parks. +- Enorme Flächen: Kilometerlange Wegenetze binden ganze Kolonnen von Reinigungskräften. +- Sicherheit: Rutschgefahr durch nasses Laub oder Abfall. + +[Secondary Product: Service] +- Versorgungslücken: An abgelegenen Attraktionen fehlt oft Gastronomie-Angebot.""", + "Gains": """[Primary Product: Cleaning Outdoor] +- Perfekte Inszenierung: Unsichtbare Reinigung in den frühen Morgenstunden sichert das perfekte Erlebnis bei Parköffnung. +- Effizienz: Ein Roboter schafft die Flächenleistung mehrerer manueller Kehrer. + +[Secondary Product: Service] +- Mobiler Verkauf: Roboter bringen Getränke/Eis direkt zu den Warteschlangen (Zusatzumsatz).""" + }, + "Energy - Grid & Utilities": { + "Pains": """[Primary Product: Security] +- Sabotage & Diebstahl: Kupferdiebstahl in Umspannwerken verursacht Millionenschäden und Versorgungsausfälle. +- Reaktionszeit: Entlegene Standorte sind für Interventionskräfte oft zu spät erreichbar. +- Sicherheitsrisiko Mensch: Alleinarbeit bei Kontrollgängen in Hochspannungsbereichen ist gefährlich.""", + "Gains": """[Primary Product: Security] +- First Responder Maschine: Roboter ist bereits vor Ort, verifiziert Alarm und schreckt Täter ab. +- KRITIS-Compliance: Lückenlose, manipulationssichere Dokumentation aller Vorfälle für Behörden. +- Arbeitsschutz: Roboter übernimmt gefährliche Routinekontrollen (z.B. Thermografie an Trafos).""" + } +} + +def get_page_id(vertical_name): + url = f"https://api.notion.com/v1/databases/{NOTION_DB_VERTICALS}/query" + payload = { + "filter": { + "property": "Vertical", + "title": { + "equals": vertical_name + } + } + } + response = requests.post(url, headers=headers, json=payload) + if response.status_code == 200: + results = response.json().get("results", []) + if results: + return results[0]["id"] + return None + +def update_page(page_id, pains, gains): + url = f"https://api.notion.com/v1/pages/{page_id}" + payload = { + "properties": { + "Pains": { + "rich_text": [{"text": {"content": pains}}] + }, + "Gains": { + "rich_text": [{"text": {"content": gains}}] + } + } + } + response = requests.patch(url, headers=headers, json=payload) + if response.status_code == 200: + print(f"✅ Updated {page_id}") + else: + print(f"❌ Failed to update {page_id}: {response.text}") + +def main(): + print("Starting update...") + for vertical, content in UPDATES.items(): + print(f"Processing '{vertical}'...") + page_id = get_page_id(vertical) + if page_id: + update_page(page_id, content["Pains"], content["Gains"]) + else: + print(f"⚠️ Vertical '{vertical}' not found in Notion.") + +if __name__ == "__main__": + main() diff --git a/company-explorer/backend/scripts/notion_maintenance/update_verticals_phase2.py b/company-explorer/backend/scripts/notion_maintenance/update_verticals_phase2.py new file mode 100644 index 00000000..9afb4c76 --- /dev/null +++ b/company-explorer/backend/scripts/notion_maintenance/update_verticals_phase2.py @@ -0,0 +1,162 @@ +import os +import requests +import json +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +NOTION_API_KEY = os.getenv("NOTION_API_KEY") +NOTION_DB_VERTICALS = "2ec88f4285448014ab38ea664b4c2b81" + +if not NOTION_API_KEY: + print("Error: NOTION_API_KEY not found.") + exit(1) + +headers = { + "Authorization": f"Bearer {NOTION_API_KEY}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" +} + +# The approved changes from ANALYSIS_AND_PROPOSAL.md for Phase 2 +UPDATES = { + "Energy - Solar/Wind": { + "Pains": """[Primary Product: Security] +- Kupfer-Diebstahl: Professionelle Banden plündern abgelegene Parks in Minuten; der Schaden durch Betriebsunterbrechung übersteigt den Materialwert oft weit. +- Interventionszeit: Bis der Wachdienst eintrifft ("Blaulicht-Fahrt"), sind die Täter längst verschwunden. +- Kostenfalle Falschalarm: Wildtiere oder wetterbedingte Störungen lösen teure, unnötige Polizeieinsätze aus.""", + "Gains": """[Primary Product: Security] +- Sofort-Verifikation: KI-gestützte Erkennung unterscheidet zuverlässig zwischen Tier und Mensch und liefert Live-Bilder in Sekunden. +- Präventive Abschreckung: Autonome Patrouillen signalisieren "Hier wird bewacht" und verhindern den Versuch. +- Lückenlose Beweissicherung: Gerichtsfeste Dokumentation von Vorfällen für Versicherung und Strafverfolgung.""" + }, + "Infrastructure - Public": { + "Pains": """[Primary Product: Cleaning Indoor] +- Zeitdruck (Turnaround): Zwischen Messe-Ende und Öffnung am nächsten Tag liegen nur wenige Stunden für eine Komplettreinigung. +- Kostenspirale: Nacht- und Wochenendzuschläge für manuelles Personal belasten das Budget massiv. +- Personalverfügbarkeit: Für Spitzenlasten (Messezeiten) ist kurzfristig kaum ausreichendes Personal zu finden. + +[Secondary Product: Cleaning Outdoor] +- Erster Eindruck: Vermüllte Vorplätze und Zufahrten schaden dem Image der Veranstaltung schon bei Ankunft.""", + "Gains": """[Primary Product: Cleaning Indoor] +- Planbare Kapazität: Roboter reinigen autonom die Kilometer langen Gänge ("Gang-Reinigung"), Personal fokussiert sich auf Stände und Details. +- Kosteneffizienz: Fixe Kosten statt variabler Zuschläge für Nachtarbeit. + +[Secondary Product: Cleaning Outdoor] +- Repräsentative Außenwirkung: Sauberer Empfangsbereich ohne permanenten Personaleinsatz.""" + }, + "Infrastructure - Transport": { + "Pains": """[Primary Product: Cleaning Indoor] +- Sicherheits-Checks: Jede externe Reinigungskraft im Sicherheitsbereich erfordert aufwändige Überprüfungen (ZÜP) und Begleitung. +- Passagier-Störung: Laute, manuelle Reinigungsmaschinen behindern Laufwege und Durchsagen im 24/7-Betrieb. +- Hochfrequenz-Verschmutzung: Kaffee-Flecken und Nässe (Winter) müssen sofort beseitigt werden, um Rutschunfälle zu vermeiden. + +[Secondary Product: Cleaning Outdoor] +- Müll-Aufkommen: Raucherbereiche und Taxi-Spuren verkommen schnell durch Zigarettenstummel und Kleinmüll.""", + "Gains": """[Primary Product: Cleaning Indoor] +- "Approved Staff": Roboter verbleibt im Sicherheitsbereich – kein täglicher Check-in/Check-out nötig. +- Silent Cleaning: Leise, autonome Navigation zwischen Passagieren stört den Betriebsablauf nicht. + +[Secondary Product: Cleaning Outdoor] +- Sauberer Transfer: Gepflegte Außenanlagen als Visitenkarte der Mobilitätsdrehscheibe.""" + }, + "Retail - Shopping Center": { + "Pains": """[Primary Product: Cleaning Indoor] +- Food-Court-Chaos: Zu Stoßzeiten kommen Reinigungskräfte mit dem Wischen von verschütteten Getränken und Essensresten kaum nach. +- Rutschfallen: Nasse Eingänge (Regen) und verschmutzte Zonen sind Haftungsrisiken für den Betreiber. +- Image-Faktor: Ein "grauer" oder fleckiger Boden senkt die Aufenthaltsqualität und damit die Verweildauer der Kunden. + +[Secondary Product: Cleaning Outdoor] +- Parkplatz-Pflege: Müll auf Parkplätzen und in Parkhäusern ist der erste negative Touchpoint für Besucher.""", + "Gains": """[Primary Product: Cleaning Indoor] +- Reaktionsschnelligkeit: Roboter sind permanent präsent und beseitigen Malheure sofort, bevor sie antrocknen. +- Hochglanz-Optik: Konstante Pflege poliert den Steinboden und sorgt für ein hochwertiges Ambiente. + +[Secondary Product: Cleaning Outdoor] +- Willkommens-Kultur: Sauberer Außenbereich lädt zum Betreten ein.""" + }, + "Leisure - Wet & Spa": { + "Pains": """[Primary Product: Cleaning Indoor] +- Rutsch-Unfälle: Staunässe auf Fliesen ist die Unfallursache Nummer 1 in Bädern – hohes Haftungsrisiko. +- Hygiene-Sensibilität: Im Barfußbereich (Umkleiden/Gänge) erwarten Gäste klinische Sauberkeit; Haare und Fussel sind "Ekel-Faktor". +- Personal-Konflikt: Fachangestellte für Bäderbetriebe sollen die Beckenaufsicht führen (Sicherheit), nicht wischen.""", + "Gains": """[Primary Product: Cleaning Indoor] +- Permanente Sicherheit: Roboter trocknen Laufwege kontinuierlich und minimieren das Rutschrisiko aktiv. +- Entlastung der Aufsicht: Bademeister können sich zu 100% auf die Sicherheit der Badegäste konzentrieren. +- Hygiene-Standard: Dokumentierte Desinfektion und Reinigung sichert Top-Bewertungen.""" + }, + "Corporate - Campus": { + "Pains": """[Primary Product: Cleaning Indoor] +- Repräsentativität: Empfangshallen und Atrien sind das Aushängeschild – sichtbarer Staub oder Schlieren wirken unprofessionell. +- Kostendruck Facility: Enorme Flächen (Flure/Verbindungsgänge) erzeugen hohe laufende Reinigungskosten. + +[Secondary Product: Cleaning Outdoor] +- Campus-Pflege: Weitläufige Außenanlagen manuell sauber zu halten, bindet unverhältnismäßig viele Ressourcen.""", + "Gains": """[Primary Product: Cleaning Indoor] +- Innovations-Statement: Einsatz von Robotik unterstreicht den technologischen Führungsanspruch des Unternehmens gegenüber Besuchern und Bewerbern. +- Konstante Qualität: Einheitliches Sauberkeitsniveau in allen Gebäudeteilen, unabhängig von Tagesform oder Krankenstand. + +[Secondary Product: Cleaning Outdoor] +- Gepflegtes Erscheinungsbild: Automatisierte Kehrleistung sorgt für repräsentative Wege und Plätze.""" + }, + "Reinigungsdienstleister": { + "Pains": """[Primary Product: Cleaning Indoor] +- Personal-Mangel & Fluktuation: Hohe "No-Show"-Quoten und ständige Neurekrutierung binden Objektleiter massiv und gefährden die Vertragserfüllung. +- Margen-Verfall: Steigende Tariflöhne bei gleichzeitigem Preisdruck der Auftraggeber lassen kaum noch Gewinn zu. +- Qualitäts-Schwankungen: Wechselndes, ungelernte Personal liefert oft unzureichende Ergebnisse, was zu Reklamationen und Kürzungen führt.""", + "Gains": """[Primary Product: Cleaning Indoor] +- Kalkulations-Sicherheit: Roboter bieten fixe Kosten statt unkalkulierbarer Krankheits- und Ausfallrisiken. +- Wettbewerbsvorteil: Mit Robotik-Konzepten punkten Dienstleister bei Ausschreibungen als Innovationsführer. +- Entlastung Objektleitung: Weniger Personal-Management bedeutet mehr Zeit für Kundenpflege und Qualitätskontrolle.""" + } +} + +def get_page_id(vertical_name): + # Try to find the page with a filter on "Vertical" property + url = f"https://api.notion.com/v1/databases/{NOTION_DB_VERTICALS}/query" + payload = { + "filter": { + "property": "Vertical", + "title": { + "equals": vertical_name + } + } + } + response = requests.post(url, headers=headers, json=payload) + if response.status_code == 200: + results = response.json().get("results", []) + if results: + return results[0]["id"] + return None + +def update_page(page_id, pains, gains): + url = f"https://api.notion.com/v1/pages/{page_id}" + payload = { + "properties": { + "Pains": { + "rich_text": [{"text": {"content": pains}}] + }, + "Gains": { + "rich_text": [{"text": {"content": gains}}] + } + } + } + response = requests.patch(url, headers=headers, json=payload) + if response.status_code == 200: + print(f"✅ Updated {page_id}") + else: + print(f"❌ Failed to update {page_id}: {response.text}") + +def main(): + print("Starting update Phase 2...") + for vertical, content in UPDATES.items(): + print(f"Processing '{vertical}'...") + page_id = get_page_id(vertical) + if page_id: + update_page(page_id, content["Pains"], content["Gains"]) + else: + print(f"⚠️ Vertical '{vertical}' not found in Notion.") + +if __name__ == "__main__": + main() diff --git a/company-explorer/backend/scripts/notion_maintenance/update_verticals_targeted.py b/company-explorer/backend/scripts/notion_maintenance/update_verticals_targeted.py new file mode 100644 index 00000000..037b3985 --- /dev/null +++ b/company-explorer/backend/scripts/notion_maintenance/update_verticals_targeted.py @@ -0,0 +1,97 @@ +import os +import requests +import json +from dotenv import load_dotenv + +load_dotenv() + +# Check for API Key +NOTION_API_KEY = os.getenv("NOTION_API_KEY") +if not NOTION_API_KEY: + try: + with open("/app/n8n_api_Token_git.txt", "r") as f: + content = f.read() + if "secret_" in content: + NOTION_API_KEY = content.strip().split('\n')[0] + except: + pass + +if not NOTION_API_KEY: + print("Error: NOTION_API_KEY not found.") + exit(1) + +NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81" +headers = {"Authorization": f"Bearer {NOTION_API_KEY}", "Notion-Version": "2022-06-28", "Content-Type": "application/json"} + +# Updates Definition +updates = { + "Energy - Grid & Utilities": { + "Pains": "[Primary Product: Security]\n- Sabotage & Diebstahl: Kupferdiebstahl in Umspannwerken verursacht Millionenschäden und Versorgungsausfälle.\n- Reaktionszeit: Entlegene Standorte sind für Interventionskräfte oft zu spät erreichbar.\n- Sicherheitsrisiko Mensch: Alleinarbeit bei Kontrollgängen in Hochspannungsbereichen ist gefährlich.\n\n[Secondary Product: Cleaning Indoor]\n- Verschmutzung in Umspannwerken: Staubablagerungen auf Böden und in technischen Bereichen können die Betriebssicherheit gefährden.\n- Manuelle Reinigung in Sicherheitsbereichen: Externes Reinigungspersonal benötigt aufwändige Sicherheitsunterweisungen und Begleitung.\n- Große Distanzen: Die Reinigung weitläufiger, oft unbemannter Anlagen ist logistisch aufwändig und wird häufig vernachlässigt.", + "Gains": "[Primary Product: Security]\n- First Responder Maschine: Roboter ist bereits vor Ort, verifiziert Alarm und schreckt Täter ab.\n- KRITIS-Compliance: Lückenlose, manipulationssichere Dokumentation aller Vorfälle für Behörden.\n- Arbeitsschutz: Roboter übernimmt gefährliche Routinekontrollen (z.B. Thermografie an Trafos).\n\n[Secondary Product: Cleaning Indoor]\n- Permanente Sauberkeit: Autonome Reinigung gewährleistet staubfreie Böden und reduziert das Risiko von technischen Störungen.\n- Zugang ohne Sicherheitsrisiko: Der Roboter ist \"Teil der Anlage\" und benötigt keine externe Sicherheitsfreigabe oder Begleitung.\n- Ressourceneffizienz: Kosteneffiziente Reinigung großer Flächen ohne Anreisezeiten für Dienstleister.", + "Secondary_Product_Name": "Cleaning Indoor (Wet Surface)" + }, + "Retail - Non-Food": { + "Pains": "[Primary Product: Cleaning Indoor]\n- Optischer Eindruck: Verschmutzte Böden, insbesondere im Premium-Segment (Möbel, Elektronik), mindern die Wertwahrnehmung der ausgestellten Produkte massiv.\n- Staubentwicklung auf großen Flächen: In Möbelhäusern und Baumärkten sammelt sich auf den riesigen Gangflächen schnell Staub, der das Einkaufserlebnis trübt.\n- Personalbindung: Verkaufsberater sollen Kunden betreuen und Umsatz generieren, statt wertvolle Zeit mit unproduktiven Kehr- oder Wischtätigkeiten zu verbringen.\n\n[Secondary Product: Service]\n- Unübersichtlichkeit: Kunden finden in großen Märkten oft nicht sofort das gesuchte Produkt und binden Personal für einfache Wegbeschreibungen.\n- Fehlende Interaktion: Passive Verkaufsflächen bieten wenig Anreiz für Kunden, sich länger aufzuhalten oder zu interagieren.", + "Gains": "[Primary Product: Cleaning Indoor]\n- Perfektes Einkaufserlebnis: Stets makellos saubere Böden unterstreichen den Qualitätsanspruch des Sortiments und laden zum Verweilen ein.\n- Fokus auf Beratung: Mitarbeiter werden von routinemäßigen Reinigungsaufgaben befreit und können sich voll auf den Kunden und den Verkauf konzentrieren.\n- Kosteneffizienz auf der Fläche: Autonome Reinigung großer Quadratmeterzahlen ist deutlich günstiger als manuelle Arbeit, besonders außerhalb der Öffnungszeiten.\n\n[Secondary Product: Service]\n- Innovativer Kundenservice: Roboter führen Kunden autonom zum gesuchten Produktregal (\"Guide-Funktion\").\n- Wow-Effekt: Der Einsatz von Robotik modernisiert das Markenimage und zieht Aufmerksamkeit auf sich." + }, + "Tech - Data Center": { + "Pains": "[Primary Product: Security]\n- Sicherheitsrisiko Zutritt: Unbefugter Zutritt in Hochsicherheitsbereiche (Serverräume, Cages) muss lückenlos detektiert und dokumentiert werden, um Zertifizierungen (ISO 27001) nicht zu gefährden.\n- Fachkräftemangel Security: Qualifiziertes Wachpersonal mit Sicherheitsüberprüfung ist extrem schwer zu finden und teuer im 24/7-Schichtbetrieb.\n- Dokumentationslücken: Manuelle Patrouillen sind fehleranfällig und Protokolle können unvollständig sein, was bei Audits zu Problemen führt.\n\n[Secondary Product: Cleaning Indoor]\n- Gefahr durch Staubpartikel: Feinstaub in Serverräumen kann Kühlsysteme verstopfen und Kurzschlüsse verursachen, was die Hardware-Lebensdauer verkürzt.\n- Sicherheitsrisiko Reinigungspersonal: Externes Reinigungspersonal in Sicherheitsbereichen erfordert ständige Begleitung und Überwachung (Vier-Augen-Prinzip), was Personal bindet.", + "Gains": "[Primary Product: Security]\n- Lückenloser Audit-Trail: Automatisierte, manipulationssichere Dokumentation aller Kontrollgänge und Ereignisse sichert Compliance-Anforderungen.\n- 24/7 Präsenz: Der Roboter ist immer im Dienst, wird nicht müde und garantiert eine konstante Überwachungsqualität ohne Schichtwechsel-Risiken.\n- Sofortige Alarmierung: Bei Anomalien (offene Rack-Tür, Wärmeentwicklung) erfolgt eine Echtzeit-Meldung an die Leitzentrale.\n\n[Secondary Product: Cleaning Indoor]\n- Maximale Hardware-Verfügbarkeit: Staubfreie Umgebung optimiert die Kühleffizienz und reduziert das Ausfallrisiko teurer Komponenten.\n- Autonome \"Trusted\" Cleaning: Der Roboter reinigt sensibelste Bereiche ohne das Risiko menschlichen Fehlverhaltens oder unbefugten Zugriffs.", + "Secondary_Product_Name": "Cleaning Indoor (Wet Surface)" + } +} + +def get_product_page_id(product_name): + url = "https://api.notion.com/v1/search" + payload = {"query": product_name, "filter": {"value": "page", "property": "object"}} + resp = requests.post(url, headers=headers, json=payload) + if resp.status_code == 200: + results = resp.json().get("results", []) + if results: return results[0]["id"] + return None + +def update_vertical(vertical_name, new_data): + url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query" + payload = {"filter": {"property": "Vertical", "title": {"contains": vertical_name}}} + resp = requests.post(url, headers=headers, json=payload) + + if resp.status_code != 200: + print(f"Error searching for {vertical_name}: {resp.text}") + return + + results = resp.json().get("results", []) + if not results: + print(f"Skipping {vertical_name} (Not found)") + return + + page_id = results[0]["id"] + print(f"Found {vertical_name} (ID: {page_id})") + + props_update = { + "Pains": {"rich_text": [{"text": {"content": new_data["Pains"]}}],}, + "Gains": {"rich_text": [{"text": {"content": new_data["Gains"]}}]} + } + + if "Secondary_Product_Name" in new_data: + prod_name = new_data["Secondary_Product_Name"] + prod_id = get_product_page_id(prod_name) + if prod_id: + print(f" Found Product ID for '{prod_name}': {prod_id}") + props_update["Secondary Product Category"] = {"relation": [{"id": prod_id}]} + props_update["Ops Focus Secondary"] = {"checkbox": True} + else: + print(f" WARNING: Product '{prod_name}' not found.") + + update_url = f"https://api.notion.com/v1/pages/{page_id}" + update_payload = {"properties": props_update} + resp_patch = requests.patch(update_url, headers=headers, json=update_payload) + + if resp_patch.status_code == 200: + print(f"✅ Successfully updated {vertical_name}") + else: + print(f"❌ Failed to update {vertical_name}: {resp_patch.text}") + +print("Starting Targeted Notion Update...") +for v_name, data in updates.items(): + update_vertical(v_name, data) +print("Done.") \ No newline at end of file diff --git a/company-explorer/backend/scripts/notion_maintenance/verify_manual_changes.py b/company-explorer/backend/scripts/notion_maintenance/verify_manual_changes.py new file mode 100644 index 00000000..3979fe90 --- /dev/null +++ b/company-explorer/backend/scripts/notion_maintenance/verify_manual_changes.py @@ -0,0 +1,88 @@ + +import os +import requests +import json +from dotenv import load_dotenv + +load_dotenv() + +# Check for API Key +NOTION_API_KEY = os.getenv("NOTION_API_KEY") +if not NOTION_API_KEY: + try: + with open("/app/n8n_api_Token_git.txt", "r") as f: + content = f.read() + if "secret_" in content: + NOTION_API_KEY = content.strip().split('\n')[0] + except: + pass + +if not NOTION_API_KEY: + print("Error: NOTION_API_KEY not found.") + exit(1) + +NOTION_DB_ID = "2ec88f4285448014ab38ea664b4c2b81" +headers = {"Authorization": f"Bearer {NOTION_API_KEY}", "Notion-Version": "2022-06-28", "Content-Type": "application/json"} + +targets = [ + "Energy - Grid & Utilities", + "Tech - Data Center", + "Retail - Non-Food" +] + +def check_vertical(vertical_name): + print(f"\n--- Checking: {vertical_name} ---") + url = f"https://api.notion.com/v1/databases/{NOTION_DB_ID}/query" + payload = {"filter": {"property": "Vertical", "title": {"contains": vertical_name}}} + resp = requests.post(url, headers=headers, json=payload) + + if resp.status_code != 200: + print(f"Error: {resp.text}") + return + + results = resp.json().get("results", []) + if not results: + print("Not found.") + return + + page = results[0] + props = page["properties"] + + # Check Pains (Start) + pains = props.get("Pains", {}).get("rich_text", []) + pains_text = "".join([t["text"]["content"] for t in pains]) + print(f"PAINS (First 100 chars): {pains_text[:100]}...") + + # Check Gains (Start) + gains = props.get("Gains", {}).get("rich_text", []) + gains_text = "".join([t["text"]["content"] for t in gains]) + print(f"GAINS (First 100 chars): {gains_text[:100]}...") + + # Check Ops Focus Secondary + ops_focus = props.get("Ops Focus: Secondary", {}).get("checkbox", False) + print(f"Ops Focus Secondary: {ops_focus}") + + # Check Secondary Product + sec_prod_rel = props.get("Secondary Product", {}).get("relation", []) + if sec_prod_rel: + prod_id = sec_prod_rel[0]["id"] + # Fetch Product Name + prod_url = f"https://api.notion.com/v1/pages/{prod_id}" + prod_resp = requests.get(prod_url, headers=headers) + if prod_resp.status_code == 200: + prod_props = prod_resp.json()["properties"] + # Try to find Name/Title + # Usually "Name" or "Product Name" + # Let's look for title type + prod_name = "Unknown" + for k, v in prod_props.items(): + if v["type"] == "title": + prod_name = "".join([t["text"]["content"] for t in v["title"]]) + print(f"Secondary Product: {prod_name}") + else: + print(f"Secondary Product ID: {prod_id} (Could not fetch name)") + else: + print("Secondary Product: None") + +for t in targets: + check_vertical(t) diff --git a/add_product_to_notion.py b/company-explorer/backend/scripts/notion_tools/add_product_to_notion.py similarity index 100% rename from add_product_to_notion.py rename to company-explorer/backend/scripts/notion_tools/add_product_to_notion.py diff --git a/check_notion_token.py b/company-explorer/backend/scripts/notion_tools/check_notion_token.py similarity index 100% rename from check_notion_token.py rename to company-explorer/backend/scripts/notion_tools/check_notion_token.py diff --git a/create_dashboard.py b/company-explorer/backend/scripts/notion_tools/create_dashboard.py similarity index 100% rename from create_dashboard.py rename to company-explorer/backend/scripts/notion_tools/create_dashboard.py diff --git a/create_feature_translator_db.py b/company-explorer/backend/scripts/notion_tools/create_feature_translator_db.py similarity index 100% rename from create_feature_translator_db.py rename to company-explorer/backend/scripts/notion_tools/create_feature_translator_db.py diff --git a/create_notion_db.py b/company-explorer/backend/scripts/notion_tools/create_notion_db.py similarity index 100% rename from create_notion_db.py rename to company-explorer/backend/scripts/notion_tools/create_notion_db.py diff --git a/debug_notion_properties.py b/company-explorer/backend/scripts/notion_tools/debug_notion_properties.py similarity index 100% rename from debug_notion_properties.py rename to company-explorer/backend/scripts/notion_tools/debug_notion_properties.py diff --git a/company-explorer/backend/scripts/notion_tools/debug_notion_schema.py b/company-explorer/backend/scripts/notion_tools/debug_notion_schema.py new file mode 100644 index 00000000..284253a0 --- /dev/null +++ b/company-explorer/backend/scripts/notion_tools/debug_notion_schema.py @@ -0,0 +1,36 @@ + +import requests +import json + +# Notion Config +try: + with open("notion_token.txt", "r") as f: + NOTION_TOKEN = f.read().strip() +except FileNotFoundError: + print("Error: notion_token.txt not found.") + exit(1) + +NOTION_VERSION = "2022-06-28" +NOTION_API_BASE_URL = "https://api.notion.com/v1" +HEADERS = { + "Authorization": f"Bearer {NOTION_TOKEN}", + "Notion-Version": NOTION_VERSION, + "Content-Type": "application/json", +} + +# DB ID from import_product.py +DB_ID = "2e288f42-8544-8113-b878-ec99c8a02a6b" + +def get_db_properties(database_id): + url = f"{NOTION_API_BASE_URL}/databases/{database_id}" + try: + response = requests.get(url, headers=HEADERS) + response.raise_for_status() + return response.json().get("properties") + except Exception as e: + print(f"Error: {e}") + return None + +props = get_db_properties(DB_ID) +if props: + print(json.dumps(props, indent=2)) diff --git a/debug_notion_search.py b/company-explorer/backend/scripts/notion_tools/debug_notion_search.py similarity index 100% rename from debug_notion_search.py rename to company-explorer/backend/scripts/notion_tools/debug_notion_search.py diff --git a/company-explorer/backend/scripts/notion_tools/debug_notion_time.py b/company-explorer/backend/scripts/notion_tools/debug_notion_time.py new file mode 100644 index 00000000..d486c2ef --- /dev/null +++ b/company-explorer/backend/scripts/notion_tools/debug_notion_time.py @@ -0,0 +1,85 @@ +import os +import json +import requests +from dotenv import load_dotenv + +load_dotenv() + +SESSION_FILE = ".dev_session/SESSION_INFO" + +def debug_notion(): + if not os.path.exists(SESSION_FILE): + print("No session file found.") + return + + with open(SESSION_FILE, "r") as f: + data = json.load(f) + + task_id = data.get("task_id") + token = data.get("token") + + print(f"Debug Info:") + print(f"Task ID: {task_id}") + print(f"Token (first 4 chars): {token[:4]}...") + + url = f"https://api.notion.com/v1/pages/{task_id}" + headers = { + "Authorization": f"Bearer {token}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" + } + + # 1. Fetch Page + print("\n--- Fetching Page Properties ---") + resp = requests.get(url, headers=headers) + if resp.status_code != 200: + print(f"Error fetching page: {resp.status_code}") + print(resp.text) + return + + page_data = resp.json() + properties = page_data.get("properties", {}) + + print(f"Found {len(properties)} properties:") + target_prop_name = "Total Duration (h)" + found_target = False + + for name, prop in properties.items(): + type_ = prop.get("type") + val = prop.get(type_) + print(f"- '{name}' ({type_}): {val}") + if name == target_prop_name: + found_target = True + + if not found_target: + print(f"\nCRITICAL: Property '{target_prop_name}' NOT found on this task!") + # Check for similar names + for name in properties.keys(): + if "duration" in name.lower() or "zeit" in name.lower() or "hours" in name.lower(): + print(f" -> Did you mean: '{name}'?") + return + + # 2. Try Update + print(f"\n--- Attempting Update of '{target_prop_name}' ---") + current_val = properties[target_prop_name].get("number") or 0.0 + print(f"Current Value: {current_val}") + + new_val = current_val + 0.01 + print(f"Updating to: {new_val}") + + update_payload = { + "properties": { + target_prop_name: {"number": new_val} + } + } + + patch_resp = requests.patch(url, headers=headers, json=update_payload) + if patch_resp.status_code == 200: + print("✅ Update Successful!") + print(f"New Value on Server: {patch_resp.json()['properties'][target_prop_name].get('number')}") + else: + print(f"❌ Update Failed: {patch_resp.status_code}") + print(patch_resp.text) + +if __name__ == "__main__": + debug_notion() diff --git a/distribute_product_data.py b/company-explorer/backend/scripts/notion_tools/distribute_product_data.py similarity index 100% rename from distribute_product_data.py rename to company-explorer/backend/scripts/notion_tools/distribute_product_data.py diff --git a/hello_notion.py b/company-explorer/backend/scripts/notion_tools/hello_notion.py similarity index 100% rename from hello_notion.py rename to company-explorer/backend/scripts/notion_tools/hello_notion.py diff --git a/import_competitive_radar.py b/company-explorer/backend/scripts/notion_tools/import_competitive_radar.py similarity index 100% rename from import_competitive_radar.py rename to company-explorer/backend/scripts/notion_tools/import_competitive_radar.py diff --git a/import_product.py b/company-explorer/backend/scripts/notion_tools/import_product.py similarity index 100% rename from import_product.py rename to company-explorer/backend/scripts/notion_tools/import_product.py diff --git a/import_single_competitor.py b/company-explorer/backend/scripts/notion_tools/import_single_competitor.py similarity index 100% rename from import_single_competitor.py rename to company-explorer/backend/scripts/notion_tools/import_single_competitor.py diff --git a/company-explorer/backend/scripts/notion_tools/inspect_persona_db.py b/company-explorer/backend/scripts/notion_tools/inspect_persona_db.py new file mode 100644 index 00000000..d0d9cc3d --- /dev/null +++ b/company-explorer/backend/scripts/notion_tools/inspect_persona_db.py @@ -0,0 +1,24 @@ +import sys +import os +import requests +import json + +NOTION_TOKEN_FILE = "/app/notion_token.txt" +PERSONAS_DB_ID = "2e288f42-8544-8113-b878-ec99c8a02a6b" + +def load_notion_token(): + with open(NOTION_TOKEN_FILE, "r") as f: + return f.read().strip() + +def query_notion_db(token, db_id): + url = f"https://api.notion.com/v1/databases/{db_id}/query" + headers = { + "Authorization": f"Bearer {token}", + "Notion-Version": "2022-06-28" + } + response = requests.post(url, headers=headers) + return response.json() + +token = load_notion_token() +data = query_notion_db(token, PERSONAS_DB_ID) +print(json.dumps(data.get("results", [])[0], indent=2)) \ No newline at end of file diff --git a/company-explorer/backend/scripts/notion_tools/inspect_persona_db_v2.py b/company-explorer/backend/scripts/notion_tools/inspect_persona_db_v2.py new file mode 100644 index 00000000..f954e69e --- /dev/null +++ b/company-explorer/backend/scripts/notion_tools/inspect_persona_db_v2.py @@ -0,0 +1,30 @@ +import sys +import os +import requests +import json + +NOTION_TOKEN_FILE = "/app/notion_token.txt" +PERSONAS_DB_ID = "30588f42-8544-80c3-8919-e22d74d945ea" + +def load_notion_token(): + with open(NOTION_TOKEN_FILE, "r") as f: + return f.read().strip() + +def query_notion_db(token, db_id): + url = f"https://api.notion.com/v1/databases/{db_id}/query" + headers = { + "Authorization": f"Bearer {token}", + "Notion-Version": "2022-06-28" + } + response = requests.post(url, headers=headers) + return response.json() + +token = load_notion_token() +data = query_notion_db(token, PERSONAS_DB_ID) +results = data.get("results", []) +for res in results: + props = res.get("properties", {}) + role = "".join([t.get("plain_text", "") for t in props.get("Role", {}).get("title", [])]) + print(f"Role: {role}") + print(json.dumps(props, indent=2)) + print("-" * 40) \ No newline at end of file diff --git a/notion_db_setup.py b/company-explorer/backend/scripts/notion_tools/notion_db_setup.py similarity index 100% rename from notion_db_setup.py rename to company-explorer/backend/scripts/notion_tools/notion_db_setup.py diff --git a/read_notion_dashboard.py b/company-explorer/backend/scripts/notion_tools/read_notion_dashboard.py similarity index 100% rename from read_notion_dashboard.py rename to company-explorer/backend/scripts/notion_tools/read_notion_dashboard.py diff --git a/company-explorer/backend/scripts/notion_tools/sync_archetypes_final.py b/company-explorer/backend/scripts/notion_tools/sync_archetypes_final.py new file mode 100644 index 00000000..685e50c8 --- /dev/null +++ b/company-explorer/backend/scripts/notion_tools/sync_archetypes_final.py @@ -0,0 +1,161 @@ + +import requests +import json +import os + +# --- Configuration --- +try: + with open("notion_token.txt", "r") as f: + NOTION_TOKEN = f.read().strip() +except FileNotFoundError: + print("Error: notion_token.txt not found.") + exit(1) + +NOTION_VERSION = "2022-06-28" +NOTION_API_BASE_URL = "https://api.notion.com/v1" +HEADERS = { + "Authorization": f"Bearer {NOTION_TOKEN}", + "Notion-Version": NOTION_VERSION, + "Content-Type": "application/json", +} + +# DB: Personas / Roles +DB_ID = "30588f42854480c38919e22d74d945ea" + +# --- Data for Archetypes --- +archetypes = [ + { + "name": "Wirtschaftlicher Entscheider", + "pains": [ + "Steigende Personalkosten im Reinigungs- und Servicebereich gefährden Profitabilität.", + "Fachkräftemangel und Schwierigkeiten bei der Stellenbesetzung.", + "Inkonsistente Qualitätsstandards schaden dem Ruf des Hauses.", + "Hoher Managementaufwand für manuelle operative Prozesse." + ], + "gains": [ + "Reduktion operativer Personalkosten um 10-25%.", + "Deutliche Abnahme der Überstunden (bis zu 50%).", + "Sicherstellung konstant hoher Qualitätsstandards.", + "Erhöhung der operativen Effizienz durch präzise Datenanalysen." + ], + "kpis": "Betriebskosten pro Einheit, Gästezufriedenheit (NPS), Mitarbeiterfluktuation.", + "positions": "Direktor, Geschäftsführer, C-Level, Einkaufsleiter." + }, + { + "name": "Operativer Entscheider", + "pains": [ + "Team ist überlastet und gestresst (Gefahr hoher Fluktuation).", + "Zu viele manuelle Routineaufgaben wie Abräumen oder Materialtransport.", + "Mangelnde Personalverfügbarkeit in Stoßzeiten führt zu Engpässen." + ], + "gains": [ + "Signifikante Entlastung des Personals von Routineaufgaben (20-40% Zeitgewinn).", + "Garantierte Reinigungszyklen unabhängig von Personalausfällen.", + "Mehr Zeit für wertschöpfende Aufgaben (Gästebetreuung, Upselling)." + ], + "kpis": "Zeitaufwand für Routineaufgaben, Abdeckungsrate der Zyklen, Servicegeschwindigkeit.", + "positions": "Leiter Housekeeping, F&B Manager, Restaurantleiter, Stationsleitung." + }, + { + "name": "Infrastruktur-Verantwortlicher", + "pains": [ + "Technische Komplexität der Integration in bestehende Infrastruktur (Aufzüge, WLAN).", + "Sorge vor hohen Ausfallzeiten und unplanmäßigen Wartungskosten.", + "Fehlendes internes Fachpersonal für die Wartung autonomer Systeme." + ], + "gains": [ + "Reibungslose Integration (20-30% schnellere Implementierung).", + "Minimierung von Ausfallzeiten um 80-90% durch proaktives Monitoring.", + "Planbare Wartung und transparente Kosten durch feste SLAs." + ], + "kpis": "System-Uptime, Implementierungszeit, Wartungskosten (TCO).", + "positions": "Technischer Leiter, Facility Manager, IT-Leiter." + }, + { + "name": "Innovations-Treiber", + "pains": [ + "Verlust der Wettbewerbsfähigkeit durch veraltete Prozesse.", + "Schwierigkeit das Unternehmen als modernen Arbeitgeber zu positionieren.", + "Statische Informations- und Marketingflächen werden oft ignoriert." + ], + "gains": [ + "Positionierung als Innovationsführer am Markt.", + "Steigerung der Kundeninteraktion um 20-30%.", + "Gewinnung wertvoller Daten zur kontinuierlichen Prozessoptimierung.", + "Erhöhte Attraktivität für junge, technikaffine Talente." + ], + "kpis": "Besucherinteraktionsrate, Anzahl Prozessinnovationen, Modernitäts-Sentiment.", + "positions": "Marketingleiter, Center Manager, CDO, Business Development." + } +] + +# --- Helper Functions --- + +def format_rich_text(text): + return {"rich_text": [{"type": "text", "text": {"content": text}}]} + +def format_title(text): + return {"title": [{"type": "text", "text": {"content": text}}]} + +def find_page(title): + url = f"{NOTION_API_BASE_URL}/databases/{DB_ID}/query" + payload = { + "filter": { + "property": "Role", + "title": {"equals": title} + } + } + resp = requests.post(url, headers=HEADERS, json=payload) + resp.raise_for_status() + results = resp.json().get("results") + return results[0] if results else None + +def create_page(properties): + url = f"{NOTION_API_BASE_URL}/pages" + payload = { + "parent": {"database_id": DB_ID}, + "properties": properties + } + resp = requests.post(url, headers=HEADERS, json=payload) + resp.raise_for_status() + print("Created.") + +def update_page(page_id, properties): + url = f"{NOTION_API_BASE_URL}/pages/{page_id}" + payload = {"properties": properties} + resp = requests.patch(url, headers=HEADERS, json=payload) + resp.raise_for_status() + print("Updated.") + +# --- Main Logic --- + +def main(): + print(f"Syncing {len(archetypes)} Personas to Notion DB {DB_ID}...") + + for p in archetypes: + print(f"Processing '{p['name']}'...") + + pains_text = "\n".join([f"- {item}" for item in p["pains"]]) + gains_text = "\n".join([f"- {item}" for item in p["gains"]]) + + properties = { + "Role": format_title(p["name"]), + "Pains": format_rich_text(pains_text), + "Gains": format_rich_text(gains_text), + "KPIs": format_rich_text(p.get("kpis", "")), + "Typische Positionen": format_rich_text(p.get("positions", "")) + } + + existing_page = find_page(p["name"]) + + if existing_page: + print(f" -> Found existing page {existing_page['id']}. Updating...") + update_page(existing_page["id"], properties) + else: + print(" -> Creating new page...") + create_page(properties) + + print("Sync complete.") + +if __name__ == "__main__": + main() diff --git a/sync_docs_to_notion.py b/company-explorer/backend/scripts/notion_tools/sync_docs_to_notion.py similarity index 100% rename from sync_docs_to_notion.py rename to company-explorer/backend/scripts/notion_tools/sync_docs_to_notion.py diff --git a/company-explorer/backend/scripts/notion_tools/sync_personas_to_notion.py b/company-explorer/backend/scripts/notion_tools/sync_personas_to_notion.py new file mode 100644 index 00000000..1fd5bdd9 --- /dev/null +++ b/company-explorer/backend/scripts/notion_tools/sync_personas_to_notion.py @@ -0,0 +1,150 @@ + +import requests +import json + +# --- Configuration --- +try: + with open("notion_token.txt", "r") as f: + NOTION_TOKEN = f.read().strip() +except FileNotFoundError: + print("Error: notion_token.txt not found.") + exit(1) + +NOTION_VERSION = "2022-06-28" +NOTION_API_BASE_URL = "https://api.notion.com/v1" +HEADERS = { + "Authorization": f"Bearer {NOTION_TOKEN}", + "Notion-Version": NOTION_VERSION, + "Content-Type": "application/json", +} + +# DB: Sector & Persona Master +DB_ID = "2e288f42-8544-8113-b878-ec99c8a02a6b" + +# --- Data --- +archetypes = [ + { + "name": "Wirtschaftlicher Entscheider", + "pains": [ + "Steigende operative Personalkosten und Fachkräftemangel gefährden die Profitabilität.", + "Unklare Amortisation (ROI) und Risiko von Fehlinvestitionen bei neuen Technologien.", + "Intransparente Folgekosten (TCO) und schwierige Budgetplanung über die Lebensdauer." + ], + "gains": [ + "Nachweisbare Senkung der operativen Kosten (10-25%) und schnelle Amortisation.", + "Sicherung der Wettbewerbsfähigkeit durch effizientere Kostenstrukturen.", + "Volle Transparenz und Planbarkeit durch klare Service-Modelle (SLAs)." + ] + }, + { + "name": "Operativer Entscheider", + "pains": [ + "Personelle Unterbesetzung führt zu Überstunden, Stress und Qualitätsmängeln.", + "Wiederkehrende Routineaufgaben binden wertvolle Fachkräfte-Ressourcen.", + "Schwierigkeit, gleichbleibend hohe Standards (Hygiene/Service) 24/7 zu garantieren." + ], + "gains": [ + "Spürbare Entlastung des Teams von Routineaufgaben (20-40%).", + "Garantierte, gleichbleibend hohe Ausführungsqualität unabhängig von der Tagesform.", + "Stabilisierung der operativen Abläufe und Kompensation von Personalausfällen." + ] + }, + { + "name": "Infrastruktur-Verantwortlicher", + "pains": [ + "Sorge vor komplexer Integration in bestehende IT- und Gebäudeinfrastruktur (WLAN, Türen, Aufzüge).", + "Risiko von hohen Ausfallzeiten und aufwändiger Fehlerbehebung ohne internes Spezialwissen.", + "Unklare Wartungsaufwände und Angst vor 'Insel-Lösungen' ohne Schnittstellen." + ], + "gains": [ + "Reibungslose, fachgerechte Integration durch Experten-Support (Plug & Play).", + "Maximale Betriebssicherheit durch proaktives Monitoring und schnelle Reaktionszeiten.", + "Zentrales Management und volle Transparenz über Systemstatus und Wartungsbedarf." + ] + }, + { + "name": "Innovations-Treiber", + "pains": [ + "Verlust der Attraktivität als moderner Arbeitgeber oder Dienstleister (Veraltetes Image).", + "Fehlende 'Wow-Effekte' in der Kundeninteraktion und mangelnde Differenzierung vom Wettbewerb.", + "Verpasste Chancen durch fehlende Datengrundlage für digitale Optimierungen." + ], + "gains": [ + "Positionierung als Innovationsführer und Steigerung der Markenattraktivität.", + "Schaffung einzigartiger Kundenerlebnisse durch sichtbare High-Tech-Lösungen.", + "Gewinnung wertvoller Daten zur kontinuierlichen Prozessoptimierung und Digitalisierung." + ] + } +] + +# --- Helper Functions --- + +def format_rich_text(text): + return {"rich_text": [{"type": "text", "text": {"content": text}}]} + +def format_title(text): + return {"title": [{"type": "text", "text": {"content": text}}]} + +def find_page(title): + url = f"{NOTION_API_BASE_URL}/databases/{DB_ID}/query" + payload = { + "filter": { + "property": "Name", + "title": {"equals": title} + } + } + resp = requests.post(url, headers=HEADERS, json=payload) + resp.raise_for_status() + results = resp.json().get("results") + return results[0] if results else None + +def create_page(properties): + url = f"{NOTION_API_BASE_URL}/pages" + payload = { + "parent": {"database_id": DB_ID}, + "properties": properties + } + resp = requests.post(url, headers=HEADERS, json=payload) + resp.raise_for_status() + print("Created.") + +def update_page(page_id, properties): + url = f"{NOTION_API_BASE_URL}/pages/{page_id}" + payload = {"properties": properties} + resp = requests.patch(url, headers=HEADERS, json=payload) + resp.raise_for_status() + print("Updated.") + +# --- Main Sync Loop --- + +def main(): + print(f"Syncing {len(archetypes)} Personas to Notion DB {DB_ID}...") + + for p in archetypes: + print(f"Processing '{p['name']}'...") + + # Format Pains/Gains as lists with bullets for Notion Text field + pains_text = "\n".join([f"- {item}" for item in p["pains"]]) + gains_text = "\n".join([f"- {item}" for item in p["gains"]]) + + properties = { + "Name": format_title(p["name"]), + "Pains": format_rich_text(pains_text), + "Gains": format_rich_text(gains_text), + # Optional: Add a tag to distinguish them from Sectors if needed? + # Currently just relying on Name uniqueness. + } + + existing_page = find_page(p["name"]) + + if existing_page: + print(f" -> Found existing page {existing_page['id']}. Updating...") + update_page(existing_page["id"], properties) + else: + print(" -> Creating new page...") + create_page(properties) + + print("Sync complete.") + +if __name__ == "__main__": + main() diff --git a/update_notion_task.py b/company-explorer/backend/scripts/notion_tools/update_notion_task.py similarity index 100% rename from update_notion_task.py rename to company-explorer/backend/scripts/notion_tools/update_notion_task.py diff --git a/company-explorer/backend/scripts/seed_marketing_data.py b/company-explorer/backend/scripts/seed_marketing_data.py new file mode 100644 index 00000000..f2dd7822 --- /dev/null +++ b/company-explorer/backend/scripts/seed_marketing_data.py @@ -0,0 +1,131 @@ +import sys +import os +import json + +# Setup Environment to import backend modules +sys.path.append(os.path.join(os.path.dirname(__file__), "../../")) +from backend.database import SessionLocal, Persona, JobRolePattern + +def seed_archetypes(): + db = SessionLocal() + print("Seeding Strategic Archetypes (Pains & Gains)...") + + # --- 1. The 4 Strategic Archetypes --- + # Based on user input and synthesis of previous specific roles + archetypes = [ + { + "name": "Operativer Entscheider", + "pains": [ + "Personelle Unterbesetzung und hohe Fluktuation führen zu Überstunden und Qualitätsmängeln.", + "Manuelle, wiederkehrende Prozesse binden wertvolle Ressourcen und senken die Effizienz.", + "Sicherstellung gleichbleibend hoher Standards (Hygiene/Service) ist bei Personalmangel kaum möglich." + ], + "gains": [ + "Spürbare Entlastung des Teams von Routineaufgaben (20-40%).", + "Garantierte, gleichbleibend hohe Ausführungsqualität rund um die Uhr.", + "Stabilisierung der operativen Abläufe unabhängig von kurzfristigen Personalausfällen." + ] + }, + { + "name": "Infrastruktur-Verantwortlicher", + "pains": [ + "Integration neuer Systeme in bestehende Gebäude/IT ist oft komplex und risikobehaftet.", + "Sorge vor hohen Ausfallzeiten und aufwändiger Fehlerbehebung ohne internes Spezialwissen.", + "Unklare Wartungsaufwände und Schnittstellenprobleme (WLAN, Aufzüge, Türen)." + ], + "gains": [ + "Reibungslose, fachgerechte Integration in die bestehende Infrastruktur.", + "Maximale Betriebssicherheit durch proaktives Monitoring und schnelle Reaktionszeiten.", + "Volle Transparenz über Systemstatus und Wartungsbedarf." + ] + }, + { + "name": "Wirtschaftlicher Entscheider", + "pains": [ + "Steigende operative Kosten (Personal, Material) drücken auf die Margen.", + "Unklare Amortisation (ROI) und Risiko von Fehlinvestitionen bei neuen Technologien.", + "Intransparente Folgekosten (TCO) über die Lebensdauer der Anlagen." + ], + "gains": [ + "Nachweisbare Senkung der operativen Kosten (10-25%).", + "Transparente und planbare Kostenstruktur (TCO) ohne versteckte Überraschungen.", + "Schneller, messbarer Return on Investment durch Effizienzsteigerung." + ] + }, + { + "name": "Innovations-Treiber", + "pains": [ + "Verlust der Wettbewerbsfähigkeit durch veraltete Prozesse und Kundenangebote.", + "Schwierigkeit, das Unternehmen als modernes, zukunftsorientiertes Brand zu positionieren.", + "Verpasste Chancen durch fehlende Datengrundlage für Optimierungen." + ], + "gains": [ + "Positionierung als Innovationsführer und Steigerung der Arbeitgeberattraktivität.", + "Nutzung modernster Technologie als sichtbares Differenzierungsmerkmal.", + "Gewinnung wertvoller Daten zur kontinuierlichen Prozessoptimierung." + ] + } + ] + + # Clear existing Personas to avoid mix-up with old granular ones + # (In production, we might want to be more careful, but here we want a clean slate for the new archetypes) + try: + db.query(Persona).delete() + db.commit() + print("Cleared old Personas.") + except Exception as e: + print(f"Warning clearing personas: {e}") + + for p_data in archetypes: + print(f"Creating Archetype: {p_data['name']}") + new_persona = Persona( + name=p_data["name"], + pains=json.dumps(p_data["pains"]), + gains=json.dumps(p_data["gains"]) + ) + db.add(new_persona) + + db.commit() + + # --- 2. Update JobRolePatterns to map to Archetypes --- + # We map the patterns to the new 4 Archetypes + + mapping_updates = [ + # Wirtschaftlicher Entscheider + {"role": "Wirtschaftlicher Entscheider", "patterns": ["geschäftsführer", "ceo", "director", "einkauf", "procurement", "finance", "cfo"]}, + + # Operativer Entscheider + {"role": "Operativer Entscheider", "patterns": ["housekeeping", "hausdame", "hauswirtschaft", "reinigung", "restaurant", "f&b", "werksleiter", "produktionsleiter", "lager", "logistik", "operations", "coo"]}, + + # Infrastruktur-Verantwortlicher + {"role": "Infrastruktur-Verantwortlicher", "patterns": ["facility", "technik", "instandhaltung", "it-leiter", "cto", "admin", "building"]}, + + # Innovations-Treiber + {"role": "Innovations-Treiber", "patterns": ["innovation", "digital", "transformation", "business dev", "marketing"]} + ] + + # Clear old mappings to prevent confusion + db.query(JobRolePattern).delete() + db.commit() + print("Cleared old JobRolePatterns.") + + for group in mapping_updates: + role_name = group["role"] + for pattern_text in group["patterns"]: + print(f"Mapping '{pattern_text}' -> '{role_name}'") + # All seeded patterns are regex contains checks + new_pattern = JobRolePattern( + pattern_type='regex', + pattern_value=pattern_text, # Stored without wildcards + role=role_name, + priority=100, # Default priority for seeded patterns + created_by='system' + ) + db.add(new_pattern) + + db.commit() + print("Archetypes and Mappings Seeded Successfully.") + db.close() + +if __name__ == "__main__": + seed_archetypes() \ No newline at end of file diff --git a/company-explorer/backend/scripts/sync_notion_industries.py b/company-explorer/backend/scripts/sync_notion_industries.py index 091347ae..ff75942a 100644 --- a/company-explorer/backend/scripts/sync_notion_industries.py +++ b/company-explorer/backend/scripts/sync_notion_industries.py @@ -67,6 +67,7 @@ def extract_select(prop): return prop.get("select", {}).get("name", "") if prop.get("select") else "" def extract_number(prop): + if not prop: return None return prop.get("number") def sync_categories(token, session): @@ -135,6 +136,11 @@ def sync_industries(token, session): industry.name = name industry.description = extract_rich_text(props.get("Definition")) + # New: Map Pains & Gains explicitly + industry.pains = extract_rich_text(props.get("Pains")) + industry.gains = extract_rich_text(props.get("Gains")) + industry.notes = extract_rich_text(props.get("Notes")) + status = extract_select(props.get("Status")) industry.status_notion = status industry.is_focus = (status == "P1 Focus Industry") @@ -147,6 +153,12 @@ def sync_industries(token, session): industry.scraper_search_term = extract_select(props.get("Scraper Search Term")) # <-- FIXED HERE industry.scraper_keywords = extract_rich_text(props.get("Scraper Keywords")) industry.standardization_logic = extract_rich_text(props.get("Standardization Logic")) + + # New Field: Ops Focus Secondary (Checkbox) + industry.ops_focus_secondary = props.get("Ops Focus: Secondary", {}).get("checkbox", False) + + # New Field: Strategy Briefing (Miller Heiman) + industry.strategy_briefing = extract_rich_text(props.get("Strategy Briefing")) # Relation: Primary Product Category relation = props.get("Primary Product Category", {}).get("relation", []) @@ -157,6 +169,16 @@ def sync_industries(token, session): industry.primary_category_id = cat.id else: logger.warning(f"Related category {related_id} not found for industry {name}") + + # Relation: Secondary Product Category + relation_sec = props.get("Secondary Product", {}).get("relation", []) + if relation_sec: + related_id = relation_sec[0]["id"] + cat = session.query(RoboticsCategory).filter(RoboticsCategory.notion_id == related_id).first() + if cat: + industry.secondary_category_id = cat.id + else: + logger.warning(f"Related Secondary category {related_id} not found for industry {name}") count += 1 diff --git a/company-explorer/backend/scripts/sync_notion_personas.py b/company-explorer/backend/scripts/sync_notion_personas.py new file mode 100644 index 00000000..ab6f5799 --- /dev/null +++ b/company-explorer/backend/scripts/sync_notion_personas.py @@ -0,0 +1,149 @@ +import sys +import os +import requests +import json +import logging + +# Add company-explorer to path (parent of backend) +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) + +from backend.database import SessionLocal, Persona, init_db +from backend.config import settings + +# Setup Logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +NOTION_TOKEN_FILE = "/app/notion_token.txt" +# Sector & Persona Master DB +PERSONAS_DB_ID = "30588f42-8544-80c3-8919-e22d74d945ea" + +VALID_ARCHETYPES = { + "Wirtschaftlicher Entscheider", + "Operativer Entscheider", + "Infrastruktur-Verantwortlicher", + "Innovations-Treiber", + "Influencer" +} + +def load_notion_token(): + try: + with open(NOTION_TOKEN_FILE, "r") as f: + return f.read().strip() + except FileNotFoundError: + logger.error(f"Notion token file not found at {NOTION_TOKEN_FILE}") + sys.exit(1) + +def query_notion_db(token, db_id): + url = f"https://api.notion.com/v1/databases/{db_id}/query" + headers = { + "Authorization": f"Bearer {token}", + "Notion-Version": "2022-06-28", + "Content-Type": "application/json" + } + results = [] + has_more = True + next_cursor = None + + while has_more: + payload = {} + if next_cursor: + payload["start_cursor"] = next_cursor + + response = requests.post(url, headers=headers, json=payload) + if response.status_code != 200: + logger.error(f"Error querying Notion DB {db_id}: {response.text}") + break + + data = response.json() + results.extend(data.get("results", [])) + has_more = data.get("has_more", False) + next_cursor = data.get("next_cursor") + + return results + +def extract_title(prop): + if not prop: return "" + return "".join([t.get("plain_text", "") for t in prop.get("title", [])]) + +def extract_rich_text(prop): + if not prop: return "" + return "".join([t.get("plain_text", "") for t in prop.get("rich_text", [])]) + +def extract_rich_text_to_list(prop): + """ + Extracts rich text and converts bullet points/newlines into a list of strings. + """ + if not prop: return [] + full_text = "".join([t.get("plain_text", "") for t in prop.get("rich_text", [])]) + + # Split by newline and clean up bullets + lines = full_text.split('\n') + cleaned_lines = [] + for line in lines: + line = line.strip() + if not line: continue + if line.startswith("- "): + line = line[2:] + elif line.startswith("• "): + line = line[2:] + cleaned_lines.append(line) + + return cleaned_lines + +def sync_personas(token, session): + logger.info("Syncing Personas from Notion...") + + pages = query_notion_db(token, PERSONAS_DB_ID) + count = 0 + + for page in pages: + props = page.get("properties", {}) + # The title property is 'Role' in the new DB, not 'Name' + name = extract_title(props.get("Role")) + + if name not in VALID_ARCHETYPES: + logger.debug(f"Skipping '{name}' (Not a target Archetype)") + continue + + logger.info(f"Processing Persona: {name}") + + pains_list = extract_rich_text_to_list(props.get("Pains")) + gains_list = extract_rich_text_to_list(props.get("Gains")) + + description = extract_rich_text(props.get("Rollenbeschreibung")) + convincing_arguments = extract_rich_text(props.get("Was ihn überzeugt")) + typical_positions = extract_rich_text(props.get("Typische Positionen")) + kpis = extract_rich_text(props.get("KPIs")) + + # Upsert Logic + persona = session.query(Persona).filter(Persona.name == name).first() + if not persona: + persona = Persona(name=name) + session.add(persona) + logger.info(f" -> Creating new entry") + else: + logger.info(f" -> Updating existing entry") + + persona.pains = json.dumps(pains_list, ensure_ascii=False) + persona.gains = json.dumps(gains_list, ensure_ascii=False) + persona.description = description + persona.convincing_arguments = convincing_arguments + persona.typical_positions = typical_positions + persona.kpis = kpis + + count += 1 + + session.commit() + logger.info(f"Sync complete. Updated {count} personas.") + +if __name__ == "__main__": + token = load_notion_token() + db = SessionLocal() + + try: + sync_personas(token, db) + except Exception as e: + logger.error(f"Sync failed: {e}", exc_info=True) + finally: + db.close() diff --git a/company-explorer/backend/scripts/sync_notion_to_ce_enhanced.py b/company-explorer/backend/scripts/sync_notion_to_ce_enhanced.py index e7b42410..513b44ed 100644 --- a/company-explorer/backend/scripts/sync_notion_to_ce_enhanced.py +++ b/company-explorer/backend/scripts/sync_notion_to_ce_enhanced.py @@ -7,7 +7,7 @@ import logging # /app/backend/scripts/sync.py -> /app sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) -from backend.database import SessionLocal, Industry, RoboticsCategory, init_db +from backend.database import SessionLocal, Industry, RoboticsCategory, Persona, init_db from dotenv import load_dotenv # Try loading from .env in root if exists @@ -76,6 +76,21 @@ def extract_number(prop): if not prop or "number" not in prop: return None return prop["number"] +def extract_rich_text_to_list(prop): + if not prop or "rich_text" not in prop: return [] + full_text = "".join([t.get("plain_text", "") for t in prop.get("rich_text", [])]) + lines = full_text.split('\n') + cleaned_lines = [] + for line in lines: + line = line.strip() + if not line: continue + if line.startswith("- "): + line = line[2:] + elif line.startswith("• "): + line = line[2:] + cleaned_lines.append(line) + return cleaned_lines + def sync(): logger.info("--- Starting Enhanced Sync ---") @@ -83,6 +98,48 @@ def sync(): init_db() session = SessionLocal() + # --- 4. Sync Personas (NEW) --- + # Sector & Persona Master ID + PERSONAS_DB_ID = "2e288f42-8544-8113-b878-ec99c8a02a6b" + VALID_ARCHETYPES = { + "Wirtschaftlicher Entscheider", + "Operativer Entscheider", + "Infrastruktur-Verantwortlicher", + "Innovations-Treiber" + } + + if PERSONAS_DB_ID: + logger.info(f"Syncing Personas from {PERSONAS_DB_ID}...") + pages = query_all(PERSONAS_DB_ID) + p_count = 0 + + # We assume Personas are cumulative, so we don't delete all first (safer for IDs) + # But we could if we wanted a clean slate. Upsert is better. + + for page in pages: + props = page["properties"] + name = extract_title(props.get("Name")) + + if name not in VALID_ARCHETYPES: + continue + + import json + pains_list = extract_rich_text_to_list(props.get("Pains")) + gains_list = extract_rich_text_to_list(props.get("Gains")) + + persona = session.query(Persona).filter(Persona.name == name).first() + if not persona: + persona = Persona(name=name) + session.add(persona) + + persona.pains = json.dumps(pains_list, ensure_ascii=False) + persona.gains = json.dumps(gains_list, ensure_ascii=False) + + p_count += 1 + + session.commit() + logger.info(f"✅ Synced {p_count} Personas.") + # 2. Sync Categories (Products) cat_db_id = find_db_id("Product Categories") or find_db_id("Products") if cat_db_id: diff --git a/company-explorer/backend/scripts/test_mapping_logic.py b/company-explorer/backend/scripts/test_mapping_logic.py new file mode 100644 index 00000000..95ce2004 --- /dev/null +++ b/company-explorer/backend/scripts/test_mapping_logic.py @@ -0,0 +1,47 @@ + +import sys +import os + +# Setup Environment +sys.path.append(os.path.join(os.path.dirname(__file__), "../../")) + +from backend.database import SessionLocal, JobRolePattern, Persona + +def test_mapping(job_title): + db = SessionLocal() + print(f"\n--- Testing Mapping for '{job_title}' ---") + + # 1. Find Role Name via JobRolePattern + role_name = None + mappings = db.query(JobRolePattern).all() + for m in mappings: + pattern_clean = m.pattern.replace("%", "").lower() + if pattern_clean in job_title.lower(): + role_name = m.role + print(f" -> Matched Pattern: '{m.pattern}' => Role: '{role_name}'") + break + + if not role_name: + print(" -> No Pattern Matched.") + return + + # 2. Find Persona via Role Name + persona = db.query(Persona).filter(Persona.name == role_name).first() + if persona: + print(f" -> Found Persona ID: {persona.id} (Name: {persona.name})") + else: + print(f" -> ERROR: Persona '{role_name}' not found in DB!") + + db.close() + +if __name__ == "__main__": + test_titles = [ + "Leiter Hauswirtschaft", + "CTO", + "Geschäftsführer", + "Head of Marketing", + "Einkaufsleiter" + ] + + for t in test_titles: + test_mapping(t) diff --git a/company-explorer/backend/scripts/test_opener_generation.py b/company-explorer/backend/scripts/test_opener_generation.py new file mode 100644 index 00000000..f39da517 --- /dev/null +++ b/company-explorer/backend/scripts/test_opener_generation.py @@ -0,0 +1,41 @@ +import sys +import os +import logging + +# Add backend path +sys.path.append(os.path.join(os.path.dirname(__file__), "../../")) + +# Mock logging +logging.basicConfig(level=logging.INFO) + +# Import Service +from backend.services.classification import ClassificationService + +def test_opener_generation(): + service = ClassificationService() + + print("\n--- TEST: Therme Erding (Primary Focus: Hygiene) ---") + op_prim = service._generate_marketing_opener( + company_name="Therme Erding", + website_text="Größte Therme der Welt, 35 Saunen, Rutschenparadies Galaxy, Wellenbad. Täglich tausende Besucher.", + industry_name="Leisure - Wet & Spa", + industry_pains="Rutschgefahr und Hygiene", + focus_mode="primary" + ) + print(f"Primary Opener: {op_prim}") + + print("\n--- TEST: Dachser Logistik (Secondary Focus: Process) ---") + op_sec = service._generate_marketing_opener( + company_name="Dachser SE", + website_text="Globaler Logistikdienstleister, Warehousing, Food Logistics, Air & Sea Logistics. Intelligent Logistics.", + industry_name="Logistics - Warehouse", + industry_pains="Effizienz und Sicherheit", + focus_mode="secondary" + ) + print(f"Secondary Opener: {op_sec}") + +if __name__ == "__main__": + try: + test_opener_generation() + except Exception as e: + print(f"Test Failed (likely due to missing env/deps): {e}") diff --git a/company-explorer/backend/scripts/trigger_analysis.py b/company-explorer/backend/scripts/trigger_analysis.py new file mode 100644 index 00000000..25d02ec5 --- /dev/null +++ b/company-explorer/backend/scripts/trigger_analysis.py @@ -0,0 +1,67 @@ +import requests +import os +import time +import argparse +import sys +import logging + +# Add the backend directory to the Python path for relative imports to work +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +# --- Configuration --- +def load_env_manual(path): + if not os.path.exists(path): + # print(f"⚠️ Warning: .env file not found at {path}") # Suppress for cleaner output in container + return + with open(path) as f: + for line in f: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + key, val = line.split('=', 1) + os.environ.setdefault(key.strip(), val.strip()) + +# Load .env (assuming it's in /app) - this needs to be run from /app or adjusted +# For docker-compose exec from project root, /app is the container's WORKDIR +load_env_manual('/app/.env') + +API_USER = os.getenv("API_USER") +API_PASS = os.getenv("API_PASSWORD") +# When run INSIDE the container, the service is reachable via localhost +CE_URL = "http://localhost:8000" +ANALYZE_ENDPOINT = f"{CE_URL}/api/enrich/analyze" + +def trigger_analysis(company_id: int): + print("="*60) + print(f"🚀 Triggering REAL analysis for Company ID: {company_id}") + print("="*60) + + payload = {"company_id": company_id} + + try: + # Added logging for API user/pass (debug only, remove in prod) + logger.debug(f"API Call to {ANALYZE_ENDPOINT} with user {API_USER}") + response = requests.post(ANALYZE_ENDPOINT, json=payload, auth=(API_USER, API_PASS), timeout=30) # Increased timeout + + if response.status_code == 200 and response.json().get("status") == "queued": + print(" ✅ SUCCESS: Analysis task has been queued on the server.") + print(" The result will be available in the database and UI shortly.") + return True + else: + print(f" ❌ FAILURE: Server responded with status {response.status_code}") + print(f" Response: {response.text}") + return False + + except requests.exceptions.RequestException as e: + print(f" ❌ FATAL: Could not connect to the server: {e}") + return False + +if __name__ == "__main__": + # Add a basic logger to the script itself for clearer output + logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + logger = logging.getLogger(__name__) + + parser = argparse.ArgumentParser(description="Trigger Company Explorer Analysis Task") + parser.add_argument("--company-id", type=int, required=True, help="ID of the company to analyze") + args = parser.parse_args() + + trigger_analysis(args.company_id) diff --git a/company-explorer/backend/scripts/upgrade_schema_v2.py b/company-explorer/backend/scripts/upgrade_schema_v2.py new file mode 100644 index 00000000..d71fe8ca --- /dev/null +++ b/company-explorer/backend/scripts/upgrade_schema_v2.py @@ -0,0 +1,33 @@ + +import sys +import os + +# Add parent directory to path to allow import of backend.database +sys.path.append(os.path.join(os.path.dirname(__file__), "../../")) + +# Import everything to ensure metadata is populated +from backend.database import engine, Base, Company, Contact, Industry, JobRolePattern, Persona, Signal, EnrichmentData, RoboticsCategory, ImportLog, ReportedMistake, MarketingMatrix + +def migrate(): + print("Migrating Database Schema...") + + try: + # Hacky migration for MarketingMatrix: Drop if exists to enforce new schema + with engine.connect() as con: + print("Dropping old MarketingMatrix table to enforce schema change...") + try: + from sqlalchemy import text + con.execute(text("DROP TABLE IF EXISTS marketing_matrix")) + print("Dropped marketing_matrix.") + except Exception as e: + print(f"Could not drop marketing_matrix: {e}") + + except Exception as e: + print(f"Pre-migration cleanup error: {e}") + + # This creates 'personas' table AND re-creates 'marketing_matrix' + Base.metadata.create_all(bind=engine) + print("Migration complete. 'personas' table created and 'marketing_matrix' refreshed.") + +if __name__ == "__main__": + migrate() diff --git a/company-explorer/backend/services/classification.py b/company-explorer/backend/services/classification.py index 3c164b6b..e8b24cef 100644 --- a/company-explorer/backend/services/classification.py +++ b/company-explorer/backend/services/classification.py @@ -5,12 +5,12 @@ import re from datetime import datetime from typing import Optional, Dict, Any, List -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, joinedload -from backend.database import Company, Industry, RoboticsCategory, EnrichmentData -from backend.lib.core_utils import call_gemini_flash, safe_eval_math, run_serp_search -from backend.services.scraping import scrape_website_content -from backend.lib.metric_parser import MetricParser +from ..database import Company, Industry, RoboticsCategory, EnrichmentData +from ..lib.core_utils import call_gemini_flash, safe_eval_math, run_serp_search +from .scraping import scrape_website_content +from ..lib.metric_parser import MetricParser logger = logging.getLogger(__name__) @@ -19,9 +19,12 @@ class ClassificationService: pass def _load_industry_definitions(self, db: Session) -> List[Industry]: - industries = db.query(Industry).all() + industries = db.query(Industry).options( + joinedload(Industry.primary_category), + joinedload(Industry.secondary_category) + ).all() if not industries: - logger.warning("No industry definitions found in DB. Classification might be limited.") + logger.warning("No industry definitions found in DB.") return industries def _get_wikipedia_content(self, db: Session, company_id: int) -> Optional[Dict[str, Any]]: @@ -49,18 +52,11 @@ Return ONLY the exact name of the industry. try: response = call_gemini_flash(prompt) if not response: return "Others" - cleaned = response.strip().replace('"', '').replace("'", "") - # Simple fuzzy match check valid_names = [i['name'] for i in industry_definitions] + ["Others"] - if cleaned in valid_names: - return cleaned - - # Fallback: Try to find name in response + if cleaned in valid_names: return cleaned for name in valid_names: - if name in cleaned: - return name - + if name in cleaned: return name return "Others" except Exception as e: logger.error(f"Classification Prompt Error: {e}") @@ -75,7 +71,7 @@ Source Text: {text_content[:6000]} Return a JSON object with: -- "raw_value": The number found (e.g. 352 or 352.0). If text says "352 Betten", extract 352. If not found, null. +- "raw_value": The number found (e.g. 352 or 352.0). If not found, null. - "raw_unit": The unit found (e.g. "Betten", "m²"). - "proof_text": A short quote from the text proving this value. @@ -84,16 +80,15 @@ JSON ONLY. try: response = call_gemini_flash(prompt, json_mode=True) if not response: return None - if isinstance(response, str): - response = response.replace("```json", "").replace("```", "").strip() - data = json.loads(response) + try: + data = json.loads(response.replace("```json", "").replace("```", "").strip()) + except: return None else: data = response - - # Basic cleanup + if isinstance(data, list) and data: data = data[0] + if not isinstance(data, dict): return None if data.get("raw_value") == "null": data["raw_value"] = None - return data except Exception as e: logger.error(f"LLM Extraction Parse Error: {e}") @@ -101,38 +96,37 @@ JSON ONLY. def _is_metric_plausible(self, metric_name: str, value: Optional[float]) -> bool: if value is None: return False - try: - val_float = float(value) - return val_float > 0 - except: - return False + try: return float(value) > 0 + except: return False def _parse_standardization_logic(self, formula: str, raw_value: float) -> Optional[float]: - if not formula or raw_value is None: - return None - formula_cleaned = formula.replace("wert", str(raw_value)).replace("Value", str(raw_value)).replace("Wert", str(raw_value)) - formula_cleaned = re.sub(r'(?i)m[²2]', '', formula_cleaned) - formula_cleaned = re.sub(r'(?i)qm', '', formula_cleaned) - formula_cleaned = re.sub(r'\s*\(.*\)\s*$', '', formula_cleaned).strip() + if not formula or raw_value is None: return None + # Clean formula: remove anything in parentheses first (often units or comments) + clean_formula = re.sub(r'\(.*?\)', '', formula.lower()) + # Replace 'wert' with the actual value + expression = clean_formula.replace("wert", str(raw_value)) + # Remove any non-math characters + expression = re.sub(r'[^0-9\.\+\-\*\/]', '', expression) try: - return safe_eval_math(formula_cleaned) + return safe_eval_math(expression) except Exception as e: - logger.error(f"Failed to parse standardization logic '{formula}' with value {raw_value}: {e}") + logger.error(f"Failed to parse logic '{formula}' with value {raw_value}: {e}") return None def _get_best_metric_result(self, results_list: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]: - if not results_list: - return None + if not results_list: return None source_priority = {"wikipedia": 0, "website": 1, "serpapi": 2} valid_results = [r for r in results_list if r.get("calculated_metric_value") is not None] - if not valid_results: - return None - valid_results.sort(key=lambda r: (source_priority.get(r.get("metric_source"), 99), -r.get("metric_confidence", 0.0))) - logger.info(f"Best result chosen: {valid_results[0]}") + if not valid_results: return None + valid_results.sort(key=lambda r: source_priority.get(r.get("metric_source"), 99)) return valid_results[0] - def _get_website_content_and_url(self, company: Company) -> Tuple[Optional[str], Optional[str]]: - return scrape_website_content(company.website), company.website + def _get_website_content_and_url(self, db: Session, company: Company) -> Tuple[Optional[str], Optional[str]]: + enrichment = db.query(EnrichmentData).filter_by(company_id=company.id, source_type="website_scrape").order_by(EnrichmentData.created_at.desc()).first() + if enrichment and enrichment.content and "raw_text" in enrichment.content: + return enrichment.content["raw_text"], company.website + content = scrape_website_content(company.website) + return content, company.website def _get_wikipedia_content_and_url(self, db: Session, company_id: int) -> Tuple[Optional[str], Optional[str]]: wiki_data = self._get_wikipedia_content(db, company_id) @@ -140,129 +134,240 @@ JSON ONLY. def _get_serpapi_content_and_url(self, company: Company, search_term: str) -> Tuple[Optional[str], Optional[str]]: serp_results = run_serp_search(f"{company.name} {company.city or ''} {search_term}") - if not serp_results: - return None, None + if not serp_results: return None, None content = " ".join([res.get("snippet", "") for res in serp_results.get("organic_results", [])]) url = serp_results.get("organic_results", [{}])[0].get("link") if serp_results.get("organic_results") else None return content, url def _extract_and_calculate_metric_cascade(self, db: Session, company: Company, industry_name: str, search_term: str, standardization_logic: Optional[str], standardized_unit: Optional[str]) -> Dict[str, Any]: - final_result = {"calculated_metric_name": search_term, "calculated_metric_value": None, "calculated_metric_unit": None, "standardized_metric_value": None, "standardized_metric_unit": standardized_unit, "metric_source": None, "metric_proof_text": None, "metric_source_url": None, "metric_confidence": 0.0, "metric_confidence_reason": "No value found in any source."} + final_result = {"calculated_metric_name": search_term, "calculated_metric_value": None, "calculated_metric_unit": None, "standardized_metric_value": None, "standardized_metric_unit": standardized_unit, "metric_source": None, "proof_text": None, "metric_source_url": None} sources = [ - ("website", self._get_website_content_and_url), - ("wikipedia", self._get_wikipedia_content_and_url), - ("serpapi", self._get_serpapi_content_and_url) + ("website", lambda: self._get_website_content_and_url(db, company)), + ("wikipedia", lambda: self._get_wikipedia_content_and_url(db, company.id)), + ("serpapi", lambda: self._get_serpapi_content_and_url(company, search_term)) ] all_source_results = [] + parser = MetricParser() for source_name, content_loader in sources: - logger.info(f"Checking {source_name} for '{search_term}' for {company.name}") + logger.info(f" -> Checking source: [{source_name.upper()}] for '{search_term}'") try: - args = (company,) if source_name == 'website' else (db, company.id) if source_name == 'wikipedia' else (company, search_term) - content_text, current_source_url = content_loader(*args) - if not content_text: - logger.info(f"No content for {source_name}.") - continue + content_text, current_source_url = content_loader() + if not content_text or len(content_text) < 100: continue llm_result = self._run_llm_metric_extraction_prompt(content_text, search_term, industry_name) - if llm_result: - llm_result['source_url'] = current_source_url - all_source_results.append((source_name, llm_result)) - except Exception as e: - logger.error(f"Error in {source_name} stage: {e}") + if llm_result and llm_result.get("proof_text"): + # Use the robust parser on the LLM's proof text or raw_value + hint = llm_result.get("raw_value") or llm_result.get("proof_text") + parsed_value = parser.extract_numeric_value(text=content_text, expected_value=str(hint)) + if parsed_value is not None: + llm_result.update({"calculated_metric_value": parsed_value, "calculated_metric_unit": llm_result.get('raw_unit'), "metric_source": source_name, "metric_source_url": current_source_url}) + all_source_results.append(llm_result) + except Exception as e: logger.error(f" -> Error in {source_name} stage: {e}") - processed_results = [] - for source_name, llm_result in all_source_results: - metric_value = llm_result.get("raw_value") - metric_unit = llm_result.get("raw_unit") - - if metric_value is not None and self._is_metric_plausible(search_term, metric_value): - standardized_value = None - if standardization_logic and metric_value is not None: - standardized_value = self._parse_standardization_logic(standardization_logic, metric_value) - - processed_results.append({ - "calculated_metric_name": search_term, - "calculated_metric_value": metric_value, - "calculated_metric_unit": metric_unit, - "standardized_metric_value": standardized_value, - "standardized_metric_unit": standardized_unit, - "metric_source": source_name, - "metric_proof_text": llm_result.get("proof_text"), - "metric_source_url": llm_result.get("source_url"), - "metric_confidence": 0.95, - "metric_confidence_reason": "Value found and extracted by LLM." - }) - else: - logger.info(f"LLM found no plausible metric for {search_term} in {source_name}.") - - best_result = self._get_best_metric_result(processed_results) - return best_result if best_result else final_result + best_result = self._get_best_metric_result(all_source_results) + if not best_result: return final_result + final_result.update(best_result) + if self._is_metric_plausible(search_term, final_result['calculated_metric_value']): + final_result['standardized_metric_value'] = self._parse_standardization_logic(standardization_logic, final_result['calculated_metric_value']) + return final_result - def extract_metrics_for_industry(self, company: Company, db: Session, industry: Industry) -> Company: - if not industry or not industry.scraper_search_term: - logger.warning(f"No metric configuration for industry '{industry.name if industry else 'None'}'") - return company - - # Improved unit derivation - if "m²" in (industry.standardization_logic or "") or "m²" in (industry.scraper_search_term or ""): - std_unit = "m²" - else: - std_unit = "Einheiten" - - metrics = self._extract_and_calculate_metric_cascade( - db, company, industry.name, industry.scraper_search_term, industry.standardization_logic, std_unit - ) - - company.calculated_metric_name = metrics["calculated_metric_name"] - company.calculated_metric_value = metrics["calculated_metric_value"] - company.calculated_metric_unit = metrics["calculated_metric_unit"] - company.standardized_metric_value = metrics["standardized_metric_value"] - company.standardized_metric_unit = metrics["standardized_metric_unit"] - company.metric_source = metrics["metric_source"] - company.metric_proof_text = metrics["metric_proof_text"] - company.metric_source_url = metrics.get("metric_source_url") - company.metric_confidence = metrics["metric_confidence"] - company.metric_confidence_reason = metrics["metric_confidence_reason"] - - company.last_classification_at = datetime.utcnow() - db.commit() - return company + def _find_direct_area(self, db: Session, company: Company, industry_name: str) -> Optional[Dict[str, Any]]: + logger.info(" -> (Helper) Running specific search for 'Fläche'...") + area_metrics = self._extract_and_calculate_metric_cascade(db, company, industry_name, search_term="Fläche", standardization_logic=None, standardized_unit="m²") + if area_metrics and area_metrics.get("calculated_metric_value") is not None: + unit = (area_metrics.get("calculated_metric_unit") or "").lower() + if any(u in unit for u in ["m²", "qm", "quadratmeter"]): + logger.info(" ✅ SUCCESS: Found direct area value.") + area_metrics['standardized_metric_value'] = area_metrics['calculated_metric_value'] + return area_metrics + return None - def reevaluate_wikipedia_metric(self, company: Company, db: Session, industry: Industry) -> Company: - logger.info(f"Re-evaluating metric for {company.name}...") - return self.extract_metrics_for_industry(company, db, industry) + def _summarize_website_for_opener(self, company_name: str, website_text: str) -> str: + """ + Creates a high-quality summary of the website content to provide + better context for the opener generation. + """ + prompt = f""" +**Rolle:** Du bist ein erfahrener B2B-Marktanalyst mit Fokus auf Facility Management und Gebäudereinigung. +**Aufgabe:** Analysiere den Website-Text des Unternehmens '{company_name}' und erstelle ein prägnantes Dossier. + +**Deine Analyse besteht aus ZWEI TEILEN:** + +**TEIL 1: Geschäftsmodell-Analyse** +1. Identifiziere die Kernprodukte und/oder Dienstleistungen des Unternehmens. +2. Fasse in 2-3 prägnanten Sätzen zusammen, was das Unternehmen macht und für welche Kunden. + +**TEIL 2: Reinigungspotenzial & Hygiene-Analyse** +1. Scanne den Text gezielt nach Hinweisen auf große Bodenflächen, Publikumsverkehr oder hohe Hygieneanforderungen (Schlüsselwörter: Reinigung, Sauberkeit, Hygiene, Bodenpflege, Verkaufsfläche, Logistikhalle, Patientenversorgung, Gästeerlebnis). +2. Bewerte das Potenzial für automatisierte Reinigungslösungen auf einer Skala (Hoch / Mittel / Niedrig). +3. Extrahiere die 1-2 wichtigsten Sätze, die diese Anforderungen oder die Größe der Einrichtung belegen. + +**Antworte AUSSCHLIESSLICH im folgenden exakten Format:** +GESCHÄFTSMODELL: +REINIGUNGSPOTENZIAL: +HYGIENE-BEWEISE: + +**Hier ist der Website-Text:** +{website_text[:5000]} +""" + try: + response = call_gemini_flash(prompt) + return response.strip() if response else "Keine Zusammenfassung möglich." + except Exception as e: + logger.error(f"Summary Error: {e}") + return "Fehler bei der Zusammenfassung." + + def _generate_marketing_opener(self, company: Company, industry: Industry, context_text: str, focus_mode: str = "primary") -> Optional[str]: + if not industry: return None + + # 1. Determine Product Category & Context + category = industry.primary_category + raw_pains = industry.pains or "" + raw_gains = industry.gains or "" + + if focus_mode == "secondary" and industry.ops_focus_secondary and industry.secondary_category: + category = industry.secondary_category + + product_name = category.name if category else "Robotik-Lösungen" + product_desc = category.description if category and category.description else "Automatisierung von operativen Prozessen" + + # Split pains/gains based on markers + def extract_segment(text, marker): + if not text: return "" + segments = re.split(r'\[(.*?)\]', text) + for i in range(1, len(segments), 2): + if marker.lower() in segments[i].lower(): + return segments[i+1].strip() + return text + + relevant_pains = extract_segment(raw_pains, "Primary Product") + relevant_gains = extract_segment(raw_gains, "Primary Product") + + if focus_mode == "secondary" and industry.ops_focus_secondary and industry.secondary_category: + relevant_pains = extract_segment(raw_pains, "Secondary Product") + relevant_gains = extract_segment(raw_gains, "Secondary Product") + + prompt = f""" +Du bist ein scharfsinniger Marktbeobachter und Branchenexperte. Formuliere eine prägnante Einleitung (genau 2 Sätze) für ein Anschreiben an das Unternehmen {company.name}. + +DEINE PERSONA: +Ein direkter Branchenkenner, der eine relevante Beobachtung teilt. Dein Ton ist faktenbasiert, professionell und absolut NICHT verkäuferisch. Dein Ziel ist es, schnell zur operativen Herausforderung überzuleiten. + +STRATEGISCHER HINTERGRUND (Nicht nennen!): +Dieses Unternehmen wird kontaktiert, weil sein Geschäftsmodell perfekt zu folgendem Bereich passt: "{product_name}" ({product_desc}). +Ziel des Schreibens ist es, die Branchen-Herausforderungen "{relevant_pains}" zu adressieren und die Mehrwerte "{relevant_gains}" zu ermöglichen. + +DEINE AUFGABE: +1. Firmenname kürzen: Kürze "{company.name}" sinnvoll (meist erste zwei Worte). Entferne UNBEDINGT Rechtsformen wie GmbH, AG, gGmbH, e.V. etc. +2. Struktur: Genau 2 flüssige Sätze. NICHT MEHR. +3. Inhalt: + - Satz 1: Eine faktenbasierte, relevante Beobachtung zum Geschäftsmodell oder einem aktuellen Fokus des Unternehmens (siehe Analyse-Dossier). Vermeide Lobhudelei und generische Floskeln. + - Satz 2: Leite direkt und prägnant zu einer spezifischen operativen Herausforderung über, die für das Unternehmen aufgrund seiner Größe oder Branche relevant ist (orientiere dich an "{relevant_pains}"). +4. STRENGES VERBOT: Nenne KEIN Produkt ("{product_name}") und biete KEINE "Lösungen", "Hilfe" oder "Zusammenarbeit" an. Der Text soll eine reine Beobachtung bleiben. +5. KEINE Anrede (kein "Sehr geehrte Damen und Herren", kein "Hallo"). + +KONTEXT (Analyse-Dossier): +{context_text} + +BEISPIEL-STIL: +"Das Kreiskrankenhaus Weilburg leistet einen bedeutenden Beitrag zur regionalen Patientenversorgung. Bei der lückenlosen Dokumentation und den strengen Hygienevorgaben im Klinikalltag ist die Aufrechterhaltung höchster Standards jedoch eine enorme operative Herausforderung." + +AUSGABE: Nur der fertige Text. +""" + try: + response = call_gemini_flash(prompt) + return response.strip().strip('"') if response else None + except Exception as e: + logger.error(f"Opener Error: {e}") + return None + + def _sync_company_address_data(self, db: Session, company: Company): + """Extracts address and VAT data from website scrape if available.""" + from ..database import EnrichmentData + enrichment = db.query(EnrichmentData).filter_by( + company_id=company.id, source_type="website_scrape" + ).order_by(EnrichmentData.created_at.desc()).first() + + if enrichment and enrichment.content and "impressum" in enrichment.content: + imp = enrichment.content["impressum"] + if imp and isinstance(imp, dict): + changed = False + # City + if imp.get("city") and not company.city: + company.city = imp.get("city") + changed = True + # Street + if imp.get("street") and not company.street: + company.street = imp.get("street") + changed = True + # Zip / PLZ + zip_val = imp.get("zip") or imp.get("plz") + if zip_val and not company.zip_code: + company.zip_code = zip_val + changed = True + # Country + if imp.get("country_code") and (not company.country or company.country == "DE"): + company.country = imp.get("country_code") + changed = True + # VAT ID + if imp.get("vat_id") and not company.crm_vat: + company.crm_vat = imp.get("vat_id") + changed = True + + if changed: + db.commit() + logger.info(f"Updated Address/VAT from Impressum for {company.name}: City={company.city}, VAT={company.crm_vat}") def classify_company_potential(self, company: Company, db: Session) -> Company: - logger.info(f"Starting classification for {company.name}...") + logger.info(f"--- Starting FULL Analysis v3.0 for {company.name} ---") + + # Ensure metadata is synced from scrape + self._sync_company_address_data(db, company) - # 1. Load Definitions industries = self._load_industry_definitions(db) - industry_defs = [{"name": i.name, "description": i.description} for i in industries] - - # 2. Get Content (Website) - website_content, _ = self._get_website_content_and_url(company) - - if not website_content: - logger.warning(f"No website content for {company.name}. Skipping classification.") + website_content, _ = self._get_website_content_and_url(db, company) + if not website_content or len(website_content) < 100: + company.status = "ENRICH_FAILED" + db.commit() return company - # 3. Classify Industry + industry_defs = [{"name": i.name, "description": i.description} for i in industries] suggested_industry_name = self._run_llm_classification_prompt(website_content, company.name, industry_defs) - logger.info(f"AI suggests industry: {suggested_industry_name}") - - # 4. Update Company - # Match back to DB object matched_industry = next((i for i in industries if i.name == suggested_industry_name), None) + if not matched_industry: + company.industry_ai = "Others" + db.commit() + return company - if matched_industry: - company.industry_ai = matched_industry.name - else: - company.industry_ai = "Others" - - # 5. Extract Metrics (Cascade) - if matched_industry: - self.extract_metrics_for_industry(company, db, matched_industry) - + company.industry_ai = matched_industry.name + logger.info(f"✅ Industry: {matched_industry.name}") + + metrics = self._find_direct_area(db, company, matched_industry.name) + if not metrics: + logger.info(" -> No direct area. Trying proxy...") + if matched_industry.scraper_search_term: + metrics = self._extract_and_calculate_metric_cascade(db, company, matched_industry.name, search_term=matched_industry.scraper_search_term, standardization_logic=matched_industry.standardization_logic, standardized_unit="m²") + + if metrics and metrics.get("calculated_metric_value"): + logger.info(f" ✅ SUCCESS: {metrics.get('calculated_metric_value')} {metrics.get('calculated_metric_unit')}") + company.calculated_metric_name = metrics.get("calculated_metric_name", matched_industry.scraper_search_term or "Fläche") + company.calculated_metric_value = metrics.get("calculated_metric_value") + company.calculated_metric_unit = metrics.get("calculated_metric_unit") + company.standardized_metric_value = metrics.get("standardized_metric_value") + company.standardized_metric_unit = metrics.get("standardized_metric_unit") + company.metric_source = metrics.get("metric_source") + company.metric_proof_text = metrics.get("proof_text") + company.metric_source_url = metrics.get("metric_source_url") + company.metric_confidence = 0.8 + company.metric_confidence_reason = "Metric processed." + + # NEW: Two-Step approach with summarization + website_summary = self._summarize_website_for_opener(company.name, website_content) + company.research_dossier = website_summary + + company.ai_opener = self._generate_marketing_opener(company, matched_industry, website_summary, "primary") + company.ai_opener_secondary = self._generate_marketing_opener(company, matched_industry, website_summary, "secondary") company.last_classification_at = datetime.utcnow() + company.status = "ENRICHED" db.commit() - + logger.info(f"--- ✅ Analysis Finished for {company.name} ---") return company \ No newline at end of file diff --git a/company-explorer/backend/services/optimization.py b/company-explorer/backend/services/optimization.py new file mode 100644 index 00000000..be7f40cd --- /dev/null +++ b/company-explorer/backend/services/optimization.py @@ -0,0 +1,157 @@ +from sqlalchemy.orm import Session +from ..database import JobRolePattern, Persona +from ..lib.core_utils import call_gemini_flash +import json +import logging +import re +import ast + +logger = logging.getLogger(__name__) + +class PatternOptimizationService: + def __init__(self, db: Session): + self.db = db + + def generate_proposals(self): + """ + Analyzes existing EXACT patterns and proposes consolidated REGEX patterns. + """ + # ... (Fetch Data logic remains) + # 1. Fetch Data + patterns = self.db.query(JobRolePattern).filter(JobRolePattern.pattern_type == "exact").all() + + # Group by Role + roles_data = {} + pattern_map = {} + + for p in patterns: + if p.role not in roles_data: + roles_data[p.role] = [] + roles_data[p.role].append(p.pattern_value) + pattern_map[p.pattern_value] = p.id + + if not roles_data: + return [] + + proposals = [] + + # 2. Analyze each role + for target_role in roles_data.keys(): + target_titles = roles_data[target_role] + + if len(target_titles) < 3: + continue + + negative_examples = [] + for other_role, titles in roles_data.items(): + if other_role != target_role: + negative_examples.extend(titles[:50]) + + # 3. Build Prompt + prompt = f""" + Act as a Regex Optimization Engine for B2B Job Titles. + + GOAL: Break down the list of 'Positive Examples' into logical CLUSTERS and create a Regex for each cluster. + TARGET ROLE: "{target_role}" + + TITLES TO COVER (Positive Examples): + {json.dumps(target_titles)} + + TITLES TO AVOID (Negative Examples - DO NOT MATCH THESE): + {json.dumps(negative_examples[:150])} + + INSTRUCTIONS: + 1. Analyze the 'Positive Examples'. Do NOT try to create one single regex for all of them. + 2. Identify distinct semantic groups. + 3. Create a Regex for EACH group. + 4. CRITICAL - CONFLICT HANDLING: + - The Regex must NOT match the 'Negative Examples'. + - Use Negative Lookahead (e.g. ^(?=.*Manager)(?!.*Facility).*) if needed. + 5. Aggressiveness: Be bold. + + OUTPUT FORMAT: + Return a valid Python List of Dictionaries. + Example: + [ + {{ + "regex": r"(?i).*pattern.*", + "explanation": "Explanation...", + "suggested_priority": 50 + }} + ] + Enclose regex patterns in r"..." strings to handle backslashes correctly. + """ + + try: + logger.info(f"Optimizing patterns for role: {target_role} (Positive: {len(target_titles)})") + + response = call_gemini_flash(prompt) # Removed json_mode=True to allow Python syntax + + # Cleanup markdown + clean_text = response.strip() + if clean_text.startswith("```python"): + clean_text = clean_text[9:-3] + elif clean_text.startswith("```json"): + clean_text = clean_text[7:-3] + elif clean_text.startswith("```"): + clean_text = clean_text[3:-3] + clean_text = clean_text.strip() + + ai_suggestions = [] + try: + # First try standard JSON + ai_suggestions = json.loads(clean_text) + except json.JSONDecodeError: + try: + # Fallback: Python AST Literal Eval (handles r"..." strings) + ai_suggestions = ast.literal_eval(clean_text) + except Exception as e: + logger.error(f"Failed to parse response for {target_role} with JSON and AST. Error: {e}") + continue + + # Verify and map back IDs + for sugg in ai_suggestions: + try: + regex_str = sugg.get('regex') + if not regex_str: continue + + # Python AST already handles r"..." decoding, so regex_str is the raw pattern + regex = re.compile(regex_str) + + # Calculate coverage locally + covered_ids = [] + covered_titles_verified = [] + + for t in target_titles: + if regex.search(t): + if t in pattern_map: + covered_ids.append(pattern_map[t]) + covered_titles_verified.append(t) + + # Calculate False Positives + false_positives = [] + for t in negative_examples: + if regex.search(t): + false_positives.append(t) + + if len(covered_ids) >= 2 and len(false_positives) == 0: + proposals.append({ + "target_role": target_role, + "regex": regex_str, + "explanation": sugg.get('explanation', 'No explanation provided'), + "priority": sugg.get('suggested_priority', 50), + "covered_pattern_ids": covered_ids, + "covered_titles": covered_titles_verified, + "false_positives": false_positives + }) + + except re.error: + logger.warning(f"AI generated invalid regex: {sugg.get('regex')}") + continue + + except Exception as e: + logger.error(f"Error optimizing patterns for {target_role}: {e}", exc_info=True) + continue + + logger.info(f"Optimization complete. Generated {len(proposals)} proposals.") + return proposals diff --git a/company-explorer/backend/services/role_mapping.py b/company-explorer/backend/services/role_mapping.py new file mode 100644 index 00000000..0a558a12 --- /dev/null +++ b/company-explorer/backend/services/role_mapping.py @@ -0,0 +1,63 @@ +import logging +import re +from sqlalchemy.orm import Session +from typing import Optional +from ..database import JobRolePattern, RawJobTitle, Persona, Contact + +logger = logging.getLogger(__name__) + +class RoleMappingService: + def __init__(self, db: Session): + self.db = db + + def get_role_for_job_title(self, job_title: str) -> Optional[str]: + """ + Finds the corresponding role for a given job title using a multi-step process. + 1. Check for exact matches. + 2. Evaluate regex patterns. + """ + if not job_title: + return None + + # Normalize job title for matching + normalized_title = job_title.lower().strip() + + # 1. Fetch all active patterns from the database, ordered by priority + patterns = self.db.query(JobRolePattern).filter( + JobRolePattern.is_active == True + ).order_by(JobRolePattern.priority.asc()).all() + + # 2. Separate patterns for easier processing + exact_patterns = {p.pattern_value.lower(): p.role for p in patterns if p.pattern_type == 'exact'} + regex_patterns = [(p.pattern_value, p.role) for p in patterns if p.pattern_type == 'regex'] + + # 3. Check for exact match first (most efficient) + if normalized_title in exact_patterns: + return exact_patterns[normalized_title] + + # 4. Evaluate regex patterns + for pattern, role in regex_patterns: + try: + if re.search(pattern, job_title, re.IGNORECASE): + return role + except re.error as e: + logger.error(f"Invalid regex for role '{role}': {pattern}. Error: {e}") + continue + + return None + + def add_or_update_unclassified_title(self, job_title: str): + """ + Logs an unclassified job title or increments its count if already present. + """ + if not job_title: + return + + entry = self.db.query(RawJobTitle).filter(RawJobTitle.title == job_title).first() + if entry: + entry.count += 1 + else: + entry = RawJobTitle(title=job_title, count=1) + self.db.add(entry) + + self.db.commit() diff --git a/company-explorer/backend/tests/test_e2e_full_flow.py b/company-explorer/backend/tests/test_e2e_full_flow.py new file mode 100644 index 00000000..3a439b16 --- /dev/null +++ b/company-explorer/backend/tests/test_e2e_full_flow.py @@ -0,0 +1,202 @@ +import requests +import time +import json +import sys +import logging + +# Configure Logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler(sys.stdout)] +) +logger = logging.getLogger("E2E-Test") + +# Configuration +API_URL = "http://172.17.0.1:8000" +API_USER = "admin" +API_PASSWORD = "gemini" + +# Test Data +TEST_COMPANY = { + "so_contact_id": 99999, + "so_person_id": 88888, + "crm_name": "Klinikum Landkreis Erding (E2E Test)", + "crm_website": "https://www.klinikum-erding.de", # Using real URL for successful discovery + "job_title": "Geschäftsführer" # Should map to Operative Decision Maker or C-Level +} + +class CompanyExplorerClient: + def __init__(self, base_url, username, password): + self.base_url = base_url + self.auth = (username, password) + self.session = requests.Session() + self.session.auth = self.auth + + def check_health(self): + try: + res = self.session.get(f"{self.base_url}/api/health", timeout=5) + res.raise_for_status() + logger.info(f"✅ Health Check Passed: {res.json()}") + return True + except Exception as e: + logger.error(f"❌ Health Check Failed: {e}") + return False + + def provision_contact(self, payload): + url = f"{self.base_url}/api/provision/superoffice-contact" + logger.info(f"🚀 Provisioning Contact: {payload['crm_name']}") + res = self.session.post(url, json=payload) + res.raise_for_status() + return res.json() + + def get_company(self, company_id): + url = f"{self.base_url}/api/companies/{company_id}" + # Retry logic for dev environment (uvicorn reloads on DB write) + for i in range(5): + try: + res = self.session.get(url) + res.raise_for_status() + return res.json() + except (requests.exceptions.ConnectionError, requests.exceptions.ChunkedEncodingError): + logger.warning(f"Connection dropped (likely uvicorn reload). Retrying {i+1}/5...") + time.sleep(2) + raise Exception("Failed to get company after retries") + + def delete_company(self, company_id): + url = f"{self.base_url}/api/companies/{company_id}" + logger.info(f"🗑️ Deleting Company ID: {company_id}") + res = self.session.delete(url) + res.raise_for_status() + return res.json() + +def run_test(): + client = CompanyExplorerClient(API_URL, API_USER, API_PASSWORD) + + if not client.check_health(): + logger.error("Aborting test due to health check failure.") + sys.exit(1) + + # 1. Trigger Provisioning (Create & Discover) + # We first send a request WITHOUT job title to just ensure company exists/starts discovery + initial_payload = { + "so_contact_id": TEST_COMPANY["so_contact_id"], + "crm_name": TEST_COMPANY["crm_name"], + "crm_website": TEST_COMPANY["crm_website"], + # No person/job title yet + } + + try: + res = client.provision_contact(initial_payload) + logger.info(f"Initial Provision Response: {res['status']}") + + # We assume the name is unique enough or we find it by listing + # But wait, how do we get the ID? + # The /provision endpoint returns status and name, but NOT the ID in the response model. + # We need to find the company ID to poll it. + # Let's search for it. + + time.sleep(1) # Wait for DB write + search_res = client.session.get(f"{API_URL}/api/companies?search={TEST_COMPANY['crm_name']}").json() + if not search_res['items']: + logger.error("❌ Company not found after creation!") + sys.exit(1) + + company = search_res['items'][0] + company_id = company['id'] + logger.info(f"Found Company ID: {company_id}") + + # 2. Poll for Status "DISCOVERED" first + max_retries = 10 + for i in range(max_retries): + company_details = client.get_company(company_id) + status = company_details['status'] + logger.info(f"Polling for Discovery ({i+1}/{max_retries}): {status}") + + if status == "DISCOVERED" or status == "ENRICHED": + break + time.sleep(2) + + # 3. Explicitly Trigger Analysis + # This ensures we don't rely on implicit side-effects of the provision endpoint + logger.info("🚀 Triggering Analysis explicitly...") + res_analyze = client.session.post(f"{API_URL}/api/enrich/analyze", json={"company_id": company_id, "force_scrape": True}) + if res_analyze.status_code != 200: + logger.warning(f"Analysis trigger warning: {res_analyze.text}") + else: + logger.info("✅ Analysis triggered.") + + # 4. Poll for Status "ENRICHED" + max_retries = 40 # Give it more time (analysis takes time) + for i in range(max_retries): + company_details = client.get_company(company_id) + status = company_details['status'] + logger.info(f"Polling for Enrichment ({i+1}/{max_retries}): {status}") + + if status == "ENRICHED": + break + time.sleep(5) + else: + logger.error("❌ Timeout waiting for Enrichment.") + # Don't exit, try to inspect what we have + + # 3. Verify Opener Logic + final_company = client.get_company(company_id) + + logger.info("--- 🔍 Verifying Analysis Results ---") + logger.info(f"Industry: {final_company.get('industry_ai')}") + logger.info(f"Metrics: {final_company.get('calculated_metric_name')} = {final_company.get('calculated_metric_value')}") + + opener_primary = final_company.get('ai_opener') + opener_secondary = final_company.get('ai_opener_secondary') + + logger.info(f"Opener (Primary): {opener_primary}") + logger.info(f"Opener (Secondary): {opener_secondary}") + + if not opener_primary or not opener_secondary: + logger.error("❌ Openers are missing!") + # sys.exit(1) # Let's continue to see if write-back works at least partially + else: + logger.info("✅ Openers generated.") + + # 4. Simulate Final Write-Back (Provisioning with Person) + full_payload = TEST_COMPANY.copy() + logger.info("🚀 Triggering Final Provisioning (Write-Back Simulation)...") + final_res = client.provision_contact(full_payload) + + logger.info(f"Final Response Status: {final_res['status']}") + logger.info(f"Role: {final_res.get('role_name')}") + logger.info(f"Subject: {final_res.get('texts', {}).get('subject')}") + + # Assertions + if final_res['status'] != "success": + logger.error(f"❌ Expected status 'success', got '{final_res['status']}'") + + if final_res.get('opener') != opener_primary: + logger.error("❌ Primary Opener mismatch in response") + + if final_res.get('opener_secondary') != opener_secondary: + logger.error("❌ Secondary Opener mismatch in response") + + if not final_res.get('texts', {}).get('intro'): + logger.warning("⚠️ Matrix Text (intro) missing (Check Seed Data)") + else: + logger.info("✅ Matrix Texts present.") + + logger.info("🎉 E2E Test Completed Successfully (mostly)!") + + except Exception as e: + logger.error(f"💥 Test Failed with Exception: {e}", exc_info=True) + finally: + # Cleanup + try: + # Re-fetch company ID if we lost it? + # We assume company_id is set if we got past step 1 + if 'company_id' in locals(): + client.delete_company(company_id) + logger.info("✅ Cleanup complete.") + except Exception as e: + logger.error(f"Cleanup failed: {e}") + +if __name__ == "__main__": + run_test() diff --git a/company-explorer/backend/tests/test_metric_extraction_hospital.py b/company-explorer/backend/tests/test_metric_extraction_hospital.py new file mode 100644 index 00000000..6f21398a --- /dev/null +++ b/company-explorer/backend/tests/test_metric_extraction_hospital.py @@ -0,0 +1,82 @@ +import unittest +import os +import sys +from unittest.mock import MagicMock, patch + +# Adjust path to allow importing from backend +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from backend.services.classification import ClassificationService +from backend.database import Company, Industry, RoboticsCategory, Session + +class TestHospitalMetricFinal(unittest.TestCase): + + def setUp(self): + self.service = ClassificationService() + self.mock_db = MagicMock(spec=Session) + + self.mock_company = Company(id=8, name="Klinikum Landkreis Erding") + self.mock_industry_hospital = Industry( + id=1, + name="Healthcare - Hospital", + scraper_search_term="Anzahl Betten", + standardization_logic="wert * 100", + primary_category=RoboticsCategory(name="Reinigungsroboter"), + secondary_category=RoboticsCategory(name="Serviceroboter"), + ) + self.mock_website_content = "Ein langer Text, der die 100-Zeichen-Prüfung besteht." + + @patch('backend.services.classification.ClassificationService._generate_marketing_opener') + @patch('backend.services.classification.ClassificationService._extract_and_calculate_metric_cascade') + @patch('backend.services.classification.ClassificationService._find_direct_area') + @patch('backend.services.classification.ClassificationService._run_llm_classification_prompt') + @patch('backend.services.classification.ClassificationService._get_website_content_and_url') + @patch('backend.services.classification.ClassificationService._load_industry_definitions') + def test_final_hospital_logic( + self, + mock_load_industries, + mock_get_website, + mock_classify, + mock_find_direct_area, + mock_extract_cascade, + mock_generate_opener + ): + print("\n--- Running Final Hospital Logic Test ---") + + # --- MOCK SETUP --- + mock_load_industries.return_value = [self.mock_industry_hospital] + mock_get_website.return_value = (self.mock_website_content, "http://mock.com") + mock_classify.return_value = "Healthcare - Hospital" + mock_find_direct_area.return_value = None # STAGE 1 MUST FAIL + + proxy_metric_result = { + "calculated_metric_name": "Anzahl Betten", + "calculated_metric_value": 352.0, + "calculated_metric_unit": "Betten", + "standardized_metric_value": 35200.0, + "standardized_metric_unit": "m²", + "metric_source": "wikipedia", + } + mock_extract_cascade.return_value = proxy_metric_result + mock_generate_opener.side_effect = ["Primary Opener", "Secondary Opener"] + + # --- EXECUTION --- + updated_company = self.service.classify_company_potential(self.mock_company, self.mock_db) + + # --- ASSERTIONS --- + mock_find_direct_area.assert_called_once() + mock_extract_cascade.assert_called_once() + + self.assertEqual(updated_company.calculated_metric_name, "Anzahl Betten") + self.assertEqual(updated_company.calculated_metric_value, 352.0) + self.assertEqual(updated_company.standardized_metric_value, 35200.0) + print(" ✅ Metrics from Stage 2 correctly applied.") + + self.assertEqual(updated_company.ai_opener, "Primary Opener") + self.assertEqual(updated_company.ai_opener_secondary, "Secondary Opener") + print(" ✅ Openers correctly applied.") + + print("\n--- ✅ PASSED: Final Hospital Logic Test. ---") + +if __name__ == '__main__': + unittest.main() diff --git a/company-explorer/backend/tests/test_opener_logic.py b/company-explorer/backend/tests/test_opener_logic.py new file mode 100644 index 00000000..6409564e --- /dev/null +++ b/company-explorer/backend/tests/test_opener_logic.py @@ -0,0 +1,110 @@ +import sys +import os +import logging +import unittest +from unittest.mock import MagicMock, patch + +# Add backend path & activate venv if possible +sys.path.insert(0, "/app") + +from backend.services.classification import ClassificationService +from backend.database import Company, Industry + +# Setup basic logging +logging.basicConfig(level=logging.INFO) + +class TestOpenerGeneration(unittest.TestCase): + + def setUp(self): + """Set up a mock environment.""" + self.service = ClassificationService() + + # Mock a database session + self.mock_db = MagicMock() + + # Mock Industry object (as if read from DB) + self.mock_industry = Industry( + name="Leisure - Wet & Spa", + pains="Hohes Unfallrisiko durch Nässe, strenge Hygiene-Anforderungen.", + ops_focus_secondary=False + ) + + # Mock Company object + self.mock_company = Company( + id=1, + name="Therme Erding", + industry_ai="Leisure - Wet & Spa" + ) + # Add the fields we are testing + self.mock_company.ai_opener = None + self.mock_company.ai_opener_secondary = None + + @patch('backend.services.classification.call_gemini_flash') + @patch('backend.services.classification.ClassificationService._run_llm_classification_prompt') + def test_dual_opener_generation(self, mock_classification_call, mock_gemini_call): + """ + Test that both primary and secondary openers are generated and stored. + """ + print("\n--- Running Integration Test for Dual Opener Generation ---") + + # --- Configure Mocks --- + # 1. Mock the classification call to return the correct industry + mock_classification_call.return_value = "Leisure - Wet & Spa" + + # 2. Mock the opener generation calls (Gemini) + mock_gemini_call.side_effect = [ + "Der reibungslose Betrieb ist entscheidend, um maximale Sicherheit zu gewährleisten.", # Mocked Primary + "Ein einzigartiges Gästeerlebnis ist der Schlüssel zum Erfolg." # Mocked Secondary + ] + + # Mock the content loader to return some text + with patch.object(self.service, '_get_website_content_and_url', return_value=("Die Therme Erding ist die größte Therme der Welt.", "http://mock.com")): + # --- Execute the Method --- + print("1. Calling classify_company_potential...") + # We patch the metric extraction to isolate the opener logic + with patch.object(self.service, 'extract_metrics_for_industry', return_value=self.mock_company): + # The method under test! + result_company = self.service.classify_company_potential(self.mock_company, self.mock_db) + + # --- Assertions --- + print("2. Verifying results...") + + # 1. Check that Gemini was called twice for the OPENERS + self.assertEqual(mock_gemini_call.call_count, 2, "❌ FAILED: AI model for OPENERS should have been called twice.") + print(" ✅ AI model for openers was called twice.") + + # 2. Check that the classification prompt was called + self.assertEqual(mock_classification_call.call_count, 1, "❌ FAILED: Classification prompt should have been called once.") + print(" ✅ Classification prompt was called once.") + + # 3. Check prompts contained the correct focus + first_call_args, _ = mock_gemini_call.call_args_list[0] + second_call_args, _ = mock_gemini_call.call_args_list[1] + self.assertIn("FOKUS: PRIMÄR-PROZESSE", first_call_args[0], "❌ FAILED: First call prompt did not have PRIMARY focus.") + print(" ✅ First opener call had PRIMARY focus.") + self.assertIn("FOKUS: SEKUNDÄR-PROZESSE", second_call_args[0], "❌ FAILED: Second call prompt did not have SECONDARY focus.") + print(" ✅ Second opener call had SECONDARY focus.") + + # 4. Check that the results were stored on the company object + self.assertIsNotNone(result_company.ai_opener, "❌ FAILED: ai_opener (primary) was not set.") + self.assertIsNotNone(result_company.ai_opener_secondary, "❌ FAILED: ai_opener_secondary was not set.") + print(" ✅ Both ai_opener fields were set on the company object.") + + # 5. Check content of the fields + self.assertIn("Sicherheit", result_company.ai_opener, "❌ FAILED: Primary opener content mismatch.") + print(f" -> Primary Opener: '{result_company.ai_opener}'") + self.assertIn("Gästeerlebnis", result_company.ai_opener_secondary, "❌ FAILED: Secondary opener content mismatch.") + print(f" -> Secondary Opener: '{result_company.ai_opener_secondary}'") + + print("\n--- ✅ PASSED: Dual Opener logic is working correctly. ---") + +if __name__ == '__main__': + # Patch the _load_industry_definitions to return our mock + with patch('backend.services.classification.ClassificationService._load_industry_definitions') as mock_load: + # Provide a description, so the classifier can match the industry + mock_load.return_value = [Industry( + name="Leisure - Wet & Spa", + description="Thermalbad, Spa, Wasserwelten, Saunen und Rutschenparks.", + pains="Hygiene" + )] + unittest.main() diff --git a/company_explorer_connector.py b/company-explorer/company_explorer_connector.py similarity index 59% rename from company_explorer_connector.py rename to company-explorer/company_explorer_connector.py index 8e8c74a3..6da60faf 100644 --- a/company_explorer_connector.py +++ b/company-explorer/company_explorer_connector.py @@ -64,10 +64,24 @@ def trigger_analysis(company_id: int) -> dict: def get_company_details(company_id: int) -> dict: """Holt die vollständigen Details zu einem Unternehmen.""" return _make_api_request("GET", f"/companies/{company_id}") + +def create_contact(company_id: int, contact_data: dict) -> dict: + """Erstellt einen neuen Kontakt für ein Unternehmen im Company Explorer.""" + payload = { + "company_id": company_id, + "first_name": contact_data.get("first_name"), + "last_name": contact_data.get("last_name"), + "email": contact_data.get("email"), + "job_title": contact_data.get("job_title"), + "role": contact_data.get("role"), + "is_primary": contact_data.get("is_primary", True) + } + return _make_api_request("POST", "/contacts", json_data=payload) -def handle_company_workflow(company_name: str) -> dict: +def handle_company_workflow(company_name: str, contact_info: dict = None) -> dict: """ Haupt-Workflow: Prüft, erstellt und reichert ein Unternehmen an. + Optional wird auch ein Kontakt angelegt. Gibt die finalen Unternehmensdaten zurück. """ print(f"Workflow gestartet für: '{company_name}'") @@ -75,70 +89,60 @@ def handle_company_workflow(company_name: str) -> dict: # 1. Prüfen, ob das Unternehmen existiert existence_check = check_company_existence(company_name) + company_id = None if existence_check.get("exists"): company_id = existence_check["company"]["id"] print(f"Unternehmen '{company_name}' (ID: {company_id}) existiert bereits.") - final_company_data = get_company_details(company_id) - return {"status": "found", "data": final_company_data} - - if "error" in existence_check: + elif "error" in existence_check: print(f"Fehler bei der Existenzprüfung: {existence_check['error']}") return {"status": "error", "message": existence_check['error']} + else: + # 2. Wenn nicht, Unternehmen erstellen + print(f"Unternehmen '{company_name}' nicht gefunden. Erstelle es...") + creation_response = create_company(company_name) + + if "error" in creation_response: + return {"status": "error", "message": creation_response['error']} + + company_id = creation_response.get("id") + print(f"Unternehmen '{company_name}' erfolgreich mit ID {company_id} erstellt.") - # 2. Wenn nicht, Unternehmen erstellen - print(f"Unternehmen '{company_name}' nicht gefunden. Erstelle es...") - creation_response = create_company(company_name) - - if "error" in creation_response: - print(f"Fehler bei der Erstellung: {creation_response['error']}") - return {"status": "error", "message": creation_response['error']} - - company_id = creation_response.get("id") - if not company_id: - print(f"Fehler: Konnte keine ID aus der Erstellungs-Antwort extrahieren: {creation_response}") - return {"status": "error", "message": "Failed to get company ID after creation."} - - print(f"Unternehmen '{company_name}' erfolgreich mit ID {company_id} erstellt.") + # 2b. Kontakt anlegen/aktualisieren (falls Info vorhanden) + if company_id and contact_info: + print(f"Lege Kontakt für {contact_info.get('last_name')} an...") + contact_res = create_contact(company_id, contact_info) + if "error" in contact_res: + print(f"Hinweis: Kontakt konnte nicht angelegt werden: {contact_res['error']}") - # 3. Discovery anstoßen - print(f"Starte Discovery für ID {company_id}...") - discovery_status = trigger_discovery(company_id) - if "error" in discovery_status: - print(f"Fehler beim Anstoßen der Discovery: {discovery_status['error']}") - return {"status": "error", "message": discovery_status['error']} - - # 4. Warten, bis Discovery eine Website gefunden hat (Polling) - max_wait_time = 30 - start_time = time.time() - website_found = False - print("Warte auf Abschluss der Discovery (max. 30s)...") - while time.time() - start_time < max_wait_time: - details = get_company_details(company_id) - if details.get("website") and details["website"] not in ["", "k.A."]: - print(f"Website gefunden: {details['website']}") - website_found = True - break - time.sleep(3) - print(".") - - if not website_found: - print("Discovery hat nach 30s keine Website gefunden. Breche Analyse ab.") - final_data = get_company_details(company_id) - return {"status": "created_discovery_timeout", "data": final_data} - - # 5. Analyse anstoßen - print(f"Starte Analyse für ID {company_id}...") - analysis_status = trigger_analysis(company_id) - if "error" in analysis_status: - print(f"Fehler beim Anstoßen der Analyse: {analysis_status['error']}") - return {"status": "error", "message": analysis_status['error']} - - print("Analyse-Prozess erfolgreich in die Warteschlange gestellt.") + # 3. Discovery anstoßen (falls Status NEW) + # Wir holen Details, um den Status zu prüfen + details = get_company_details(company_id) + if details.get("status") == "NEW": + print(f"Starte Discovery für ID {company_id}...") + trigger_discovery(company_id) + + # 4. Warten, bis Discovery eine Website gefunden hat (Polling) + max_wait_time = 30 + start_time = time.time() + website_found = False + print("Warte auf Abschluss der Discovery (max. 30s)...") + while time.time() - start_time < max_wait_time: + details = get_company_details(company_id) + if details.get("website") and details["website"] not in ["", "k.A."]: + print(f"Website gefunden: {details['website']}") + website_found = True + break + time.sleep(3) + print(".") + + # 5. Analyse anstoßen (falls Website da, aber noch nicht ENRICHED) + if details.get("website") and details["website"] not in ["", "k.A."] and details.get("status") != "ENRICHED": + print(f"Starte Analyse für ID {company_id}...") + trigger_analysis(company_id) # 6. Finale Daten abrufen und zurückgeben final_company_data = get_company_details(company_id) - - return {"status": "created_and_enriched", "data": final_company_data} + return {"status": "synced", "data": final_company_data} if __name__ == "__main__": diff --git a/Puma_m20_2026-01-08.md b/company-explorer/docs/Puma_m20_2026-01-08.md similarity index 100% rename from Puma_m20_2026-01-08.md rename to company-explorer/docs/Puma_m20_2026-01-08.md diff --git a/Supplyon.md b/company-explorer/docs/Supplyon.md similarity index 100% rename from Supplyon.md rename to company-explorer/docs/Supplyon.md diff --git a/company-explorer/docs/case_study_djh_waldbröl.md b/company-explorer/docs/case_study_djh_waldbröl.md new file mode 100644 index 00000000..ed69d0dc --- /dev/null +++ b/company-explorer/docs/case_study_djh_waldbröl.md @@ -0,0 +1,52 @@ +# Case Study: DJH Landesverband Rheinland e.V. – Panarbora + +**Naturerlebnis trifft High-Tech: Wie Panarbora mit Robotik dem Personalmangel trotzt** + +Der Naturerlebnispark Panarbora in Waldbröl ist ein Leuchtturmprojekt des DJH Landesverbandes Rheinland e.V. Auf einer Fläche von rund elf Fußballfeldern kombiniert die Anlage Abenteuer, Umweltbildung und außergewöhnliche Übernachtungsmöglichkeiten – von Baumhäusern bis zu globalen Dörfern. Doch wo jährlich 60.000 bis 70.000 Besucher die Natur erleben, hinterlassen Wetter und Frequenz ihre Spuren. Um den steigenden Qualitätsansprüchen trotz Personalmangels gerecht zu werden, setzt Panarbora seit Juli 2025 auf innovative Robotik-Lösungen von RoboPlanet. + +--- + +## Die Ausgangssituation: Herausforderungen im Parkbetrieb + +Die Reinigung eines so weitläufigen und vielseitigen Areals stellt das Team vor komplexe Aufgaben: + +* **Dezentrale Gebäudestruktur:** Neben dem zentralen Infoportal mit Gastronomie verteilen sich die Unterkünfte (Baumhäuser, Globale Dörfer) über das gesamte Gelände. +* **Wetterabhängiger Reinigungsbedarf:** Matsch, Regen und der hohe Durchlauf von Tagesgästen sorgen für extrem schwankende Verschmutzungsgrade in den öffentlichen Bereichen. +* **Akuter Personalmangel:** Wie in der gesamten Branche führt der Mangel an Reinigungskräften zu einer stetigen Überlastung des Bestandspersonals, insbesondere in den Spitzenzeiten der Saison. +* **Wachsender Qualitätsanspruch:** Die Erwartungshaltung der Gäste an Sauberkeit und Hygiene steigt kontinuierlich – ein Standard, der manuell kaum noch zu halten war. + +> *"Unser Qualitätsanspruch wächst mit den steigenden Anforderungen unserer Gäste. Gleichzeitig müssen wir unser Team vor Überlastung schützen."* +> – **Bernd Claessen**, Regionalleiter DJH Landesverband Rheinland e.V. + +--- + +## Der Lösungsansatz: Automatisierung mit Augenmaß + +Nach einer intensiven Beratungsphase und einer erfolgreichen Vorführung vor Ort entschied sich das Management für einen Pilotversuch. Bereits zwei bis drei Wochen nach dem ersten Kontakt startete im Juli 2025 die Implementierung. + +* **Technologie:** Einsatz des **Gausium Phantas**. Dieser kompakte Saug-Wisch-Roboter ist speziell für dynamische Umgebungen konzipiert. +* **Einsatzbereiche:** Der Phantas übernimmt die tägliche Bodenreinigung in den stark frequentierten Zonen: Aufenthaltsraum, Frühstücksraum, Rezeption und Büros. +* **Strategie:** Der Roboter arbeitet nicht *statt*, sondern *mit* dem Menschen. Er übernimmt die repetitiven Großflächen, sodass sich das Personal auf Detailarbeiten und die Pflege der dezentralen Unterkünfte konzentrieren kann. +* **Ausblick:** Aufgrund der positiven Erfahrungen wird aktuell der Einsatz des **MT1 Max** für die Außenbereiche geprüft, um auch dort die Wegekehrung zu automatisieren. + +--- + +## Ergebnisse und Mehrwert + +Ein halbes Jahr nach der Einführung (Stand März 2026) zieht Panarbora eine positive Bilanz: + +* **Spürbare Entlastung:** Das Reinigungsteam wird von der zeitintensiven Bodenpflege in den Zentralbereichen befreit. +* **Konstante Sauberkeit:** Unabhängig von Krankheitswellen oder Personalengpässen wird die Grundsauberkeit in den Foyers und Gastronomiebereichen täglich sichergestellt. +* **Hohe Akzeptanz:** Sowohl bei den Mitarbeitern als auch bei den Gästen stößt die "helfende Hand" auf große Begeisterung. +* **Planungssicherheit:** Die Reinigungszyklen sind nun taktet und verlässlich, was das Management der Gesamtanlage erleichtert. + +--- + +## ZDF: Zahlen, Daten, Fakten (Prognose) + +*Durch den Einsatz des Gausium Phantas werden folgende Effizienzgewinne realisiert:* + +* **Zeiteinsparung:** Ca. 2-3 Stunden manuelle Bodenreinigung pro Tag werden automatisiert. +* **Wassereinsparung:** Reduktion des Wasserverbrauchs durch das intelligente Recyclingsystem des Roboters (im Vergleich zum manuellen Moppen). +* **Chemieeinsparung:** Präzise Dosierung minimiert den Reinigungsmittelverbrauch. +* **Energie:** Hocheffizienter Betrieb mit langer Laufzeit für maximale Flächenleistung pro Akkuladung. diff --git a/yamaichi_lokal.md b/company-explorer/docs/yamaichi_lokal.md similarity index 100% rename from yamaichi_lokal.md rename to company-explorer/docs/yamaichi_lokal.md diff --git a/yamaichi_neu.md b/company-explorer/docs/yamaichi_neu.md similarity index 100% rename from yamaichi_neu.md rename to company-explorer/docs/yamaichi_neu.md diff --git a/company-explorer/fix_missing_columns.py b/company-explorer/fix_missing_columns.py new file mode 100644 index 00000000..66009398 --- /dev/null +++ b/company-explorer/fix_missing_columns.py @@ -0,0 +1,95 @@ +import sqlite3 +import os + +DB_PATH = "/data/companies_v3_fixed_2.db" + +# Fallback for local testing +if not os.path.exists(DB_PATH): + DB_PATH = "company-explorer/companies_v3_fixed_2.db" + +def add_column(cursor, table, column, type_def): + try: + cursor.execute(f"ALTER TABLE {table} ADD COLUMN {column} {type_def}") + print(f"✅ Added {table}.{column}") + except sqlite3.OperationalError as e: + if "duplicate column name" in str(e).lower(): + print(f"ℹ️ {table}.{column} already exists") + else: + print(f"❌ Error adding {table}.{column}: {e}") + +def migrate(): + print(f"Starting migration for {DB_PATH}...") + if not os.path.exists(DB_PATH): + print(f"❌ Database not found at {DB_PATH}") + return + + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # Columns for 'companies' table + company_cols = [ + ("street", "TEXT"), + ("zip_code", "TEXT"), + ("city", "TEXT"), + ("country", "TEXT DEFAULT 'DE'"), + ("calculated_metric_name", "TEXT"), + ("calculated_metric_value", "FLOAT"), + ("calculated_metric_unit", "TEXT"), + ("standardized_metric_value", "FLOAT"), + ("standardized_metric_unit", "TEXT"), + ("metric_source", "TEXT"), + ("metric_proof_text", "TEXT"), + ("metric_source_url", "TEXT"), + ("metric_confidence", "FLOAT"), + ("metric_confidence_reason", "TEXT"), + ("ai_opener", "TEXT"), + ("ai_opener_secondary", "TEXT"), + ("research_dossier", "TEXT") + ] + + # Columns for 'industries' table + industry_cols = [ + ("status_notion", "TEXT"), + ("is_focus", "BOOLEAN DEFAULT 0"), + ("pains", "TEXT"), + ("gains", "TEXT"), + ("notes", "TEXT"), + ("priority", "TEXT"), + ("ops_focus_secondary", "BOOLEAN DEFAULT 0"), + ("strategy_briefing", "TEXT"), + ("metric_type", "TEXT"), + ("min_requirement", "FLOAT"), + ("whale_threshold", "FLOAT"), + ("proxy_factor", "FLOAT"), + ("scraper_search_term", "TEXT"), + ("scraper_keywords", "TEXT"), + ("standardization_logic", "TEXT"), + ("primary_category_id", "INTEGER"), + ("secondary_category_id", "INTEGER") + ] + + # Columns for 'contacts' table + contact_cols = [ + ("so_contact_id", "INTEGER"), + ("so_person_id", "INTEGER"), + ("role", "TEXT"), + ("status", "TEXT"), + ("unsubscribe_token", "TEXT"), + ("is_primary", "BOOLEAN DEFAULT 0") + ] + + for col, dtype in company_cols: + add_column(cursor, "companies", col, dtype) + + for col, dtype in industry_cols: + add_column(cursor, "industries", col, dtype) + + for col, dtype in contact_cols: + add_column(cursor, "contacts", col, dtype) + + conn.commit() + conn.close() + print("Migration complete.") + +if __name__ == "__main__": + migrate() diff --git a/company-explorer/frontend/dist/assets/index-BgxQoHsm.css b/company-explorer/frontend/dist/assets/index-BgxQoHsm.css new file mode 100644 index 00000000..2c7cffc8 --- /dev/null +++ b/company-explorer/frontend/dist/assets/index-BgxQoHsm.css @@ -0,0 +1 @@ +*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.inset-y-0{top:0;bottom:0}.left-2\.5{left:.625rem}.left-3{left:.75rem}.right-0{right:0}.right-2{right:.5rem}.top-0{top:0}.top-1\/2{top:50%}.top-2{top:.5rem}.top-2\.5{top:.625rem}.z-10{z-index:10}.z-50{z-index:50}.z-\[60\]{z-index:60}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-8{margin-right:2rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.line-clamp-3{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3}.line-clamp-4{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4}.block{display:block}.inline{display:inline}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-20{height:5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-\[calc\(100vh-4rem\)\]{height:calc(100vh - 4rem)}.h-full{height:100%}.max-h-40{max-height:10rem}.max-h-\[85vh\]{max-height:85vh}.max-h-\[90vh\]{max-height:90vh}.min-h-screen{min-height:100vh}.w-1\/3{width:33.333333%}.w-1\/4{width:25%}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-20{width:5rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-40{width:10rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-\[1\.25rem\]{min-width:1.25rem}.min-w-\[200px\]{min-width:200px}.min-w-full{min-width:100%}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.max-w-\[150px\]{max-width:150px}.max-w-lg{max-width:32rem}.flex-1{flex:1 1 0%}.flex-shrink-0,.shrink-0{flex-shrink:0}.table-fixed{table-layout:fixed}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-help{cursor:help}.cursor-pointer{cursor:pointer}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-y-4{row-gap:1rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-slate-100>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(241 245 249 / var(--tw-divide-opacity, 1))}.divide-slate-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(226 232 240 / var(--tw-divide-opacity, 1))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-line{white-space:pre-line}.whitespace-pre-wrap{white-space:pre-wrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-bl{border-bottom-left-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-l-4{border-left-width:4px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-dotted{border-style:dotted}.border-blue-100{--tw-border-opacity: 1;border-color:rgb(219 234 254 / var(--tw-border-opacity, 1))}.border-blue-200{--tw-border-opacity: 1;border-color:rgb(191 219 254 / var(--tw-border-opacity, 1))}.border-blue-300{--tw-border-opacity: 1;border-color:rgb(147 197 253 / var(--tw-border-opacity, 1))}.border-blue-400\/30{border-color:#60a5fa4d}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-blue-500\/30{border-color:#3b82f64d}.border-green-100{--tw-border-opacity: 1;border-color:rgb(220 252 231 / var(--tw-border-opacity, 1))}.border-green-200{--tw-border-opacity: 1;border-color:rgb(187 247 208 / var(--tw-border-opacity, 1))}.border-green-400\/30{border-color:#4ade804d}.border-orange-100{--tw-border-opacity: 1;border-color:rgb(255 237 213 / var(--tw-border-opacity, 1))}.border-orange-200{--tw-border-opacity: 1;border-color:rgb(254 215 170 / var(--tw-border-opacity, 1))}.border-orange-400\/30{border-color:#fb923c4d}.border-purple-100{--tw-border-opacity: 1;border-color:rgb(243 232 255 / var(--tw-border-opacity, 1))}.border-purple-200{--tw-border-opacity: 1;border-color:rgb(233 213 255 / var(--tw-border-opacity, 1))}.border-purple-400\/30{border-color:#c084fc4d}.border-red-100{--tw-border-opacity: 1;border-color:rgb(254 226 226 / var(--tw-border-opacity, 1))}.border-slate-100{--tw-border-opacity: 1;border-color:rgb(241 245 249 / var(--tw-border-opacity, 1))}.border-slate-200{--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity, 1))}.border-slate-300{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity, 1))}.border-slate-400{--tw-border-opacity: 1;border-color:rgb(148 163 184 / var(--tw-border-opacity, 1))}.border-slate-700{--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity, 1))}.border-slate-800{--tw-border-opacity: 1;border-color:rgb(30 41 59 / var(--tw-border-opacity, 1))}.border-transparent{border-color:transparent}.border-yellow-200{--tw-border-opacity: 1;border-color:rgb(254 240 138 / var(--tw-border-opacity, 1))}.border-l-slate-400{--tw-border-opacity: 1;border-left-color:rgb(148 163 184 / var(--tw-border-opacity, 1))}.bg-black\/50{background-color:#00000080}.bg-black\/60{background-color:#0009}.bg-black\/70{background-color:#000000b3}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.bg-blue-50\/50{background-color:#eff6ff80}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-blue-600\/20{background-color:#2563eb33}.bg-blue-900\/20{background-color:#1e3a8a33}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-green-50\/50{background-color:#f0fdf480}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-green-900\/20{background-color:#14532d33}.bg-orange-100{--tw-bg-opacity: 1;background-color:rgb(255 237 213 / var(--tw-bg-opacity, 1))}.bg-orange-50{--tw-bg-opacity: 1;background-color:rgb(255 247 237 / var(--tw-bg-opacity, 1))}.bg-orange-600{--tw-bg-opacity: 1;background-color:rgb(234 88 12 / var(--tw-bg-opacity, 1))}.bg-orange-900\/20{background-color:#7c2d1233}.bg-purple-100{--tw-bg-opacity: 1;background-color:rgb(243 232 255 / var(--tw-bg-opacity, 1))}.bg-purple-50{--tw-bg-opacity: 1;background-color:rgb(250 245 255 / var(--tw-bg-opacity, 1))}.bg-purple-900\/20{background-color:#581c8733}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-50\/50{background-color:#fef2f280}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-slate-100{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.bg-slate-100\/50{background-color:#f1f5f980}.bg-slate-200{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity, 1))}.bg-slate-300{--tw-bg-opacity: 1;background-color:rgb(203 213 225 / var(--tw-bg-opacity, 1))}.bg-slate-50{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity, 1))}.bg-slate-50\/50{background-color:#f8fafc80}.bg-slate-50\/80{background-color:#f8fafccc}.bg-slate-800{--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity, 1))}.bg-slate-800\/30{background-color:#1e293b4d}.bg-slate-900{--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity, 1))}.bg-slate-900\/50{background-color:#0f172a80}.bg-slate-950{--tw-bg-opacity: 1;background-color:rgb(2 6 23 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-yellow-100{--tw-bg-opacity: 1;background-color:rgb(254 249 195 / var(--tw-bg-opacity, 1))}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity, 1))}.fill-current{fill:currentColor}.fill-red-500{fill:#ef4444}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-12{padding:3rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-9{padding-left:2.25rem;padding-right:2.25rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-3\.5{padding-top:.875rem;padding-bottom:.875rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-2{padding-bottom:.5rem}.pl-10{padding-left:2.5rem}.pl-2{padding-left:.5rem}.pl-3{padding-left:.75rem}.pl-8{padding-left:2rem}.pr-12{padding-right:3rem}.pr-2{padding-right:.5rem}.pr-4{padding-right:1rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.pt-5{padding-top:1.25rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[9px\]{font-size:9px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.leading-relaxed{line-height:1.625}.leading-tight{line-height:1.25}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-blue-400\/80{color:#60a5facc}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-blue-700{--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity, 1))}.text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity, 1))}.text-orange-400{--tw-text-opacity: 1;color:rgb(251 146 60 / var(--tw-text-opacity, 1))}.text-orange-500{--tw-text-opacity: 1;color:rgb(249 115 22 / var(--tw-text-opacity, 1))}.text-orange-600{--tw-text-opacity: 1;color:rgb(234 88 12 / var(--tw-text-opacity, 1))}.text-orange-700{--tw-text-opacity: 1;color:rgb(194 65 12 / var(--tw-text-opacity, 1))}.text-orange-800{--tw-text-opacity: 1;color:rgb(154 52 18 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-purple-500{--tw-text-opacity: 1;color:rgb(168 85 247 / var(--tw-text-opacity, 1))}.text-purple-700{--tw-text-opacity: 1;color:rgb(126 34 206 / var(--tw-text-opacity, 1))}.text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-red-500\/80{color:#ef4444cc}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity, 1))}.text-slate-100{--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity, 1))}.text-slate-200{--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}.text-slate-300{--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}.text-slate-400{--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.text-slate-600{--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1))}.text-slate-700{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity, 1))}.text-slate-800{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1))}.text-slate-900{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-300{--tw-text-opacity: 1;color:rgb(253 224 71 / var(--tw-text-opacity, 1))}.text-yellow-500{--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity, 1))}.text-yellow-600{--tw-text-opacity: 1;color:rgb(202 138 4 / var(--tw-text-opacity, 1))}.text-yellow-700{--tw-text-opacity: 1;color:rgb(161 98 7 / var(--tw-text-opacity, 1))}.text-yellow-800{--tw-text-opacity: 1;color:rgb(133 77 14 / var(--tw-text-opacity, 1))}.line-through{text-decoration-line:line-through}.opacity-0{opacity:0}.opacity-10{opacity:.1}.opacity-20{opacity:.2}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-blue-500\/20{--tw-shadow-color: rgb(59 130 246 / .2);--tw-shadow: var(--tw-shadow-colored)}.shadow-blue-900\/10{--tw-shadow-color: rgb(30 58 138 / .1);--tw-shadow: var(--tw-shadow-colored)}.shadow-blue-900\/20{--tw-shadow-color: rgb(30 58 138 / .2);--tw-shadow: var(--tw-shadow-colored)}.shadow-green-500\/20{--tw-shadow-color: rgb(34 197 94 / .2);--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-md{--tw-backdrop-blur: blur(12px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:#1e293b}::-webkit-scrollbar-thumb{background:#475569;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#64748b}.hover\:line-clamp-none:hover{overflow:visible;display:block;-webkit-box-orient:horizontal;-webkit-line-clamp:none}.hover\:scale-110:hover{--tw-scale-x: 1.1;--tw-scale-y: 1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-slate-300:hover{--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity, 1))}.hover\:bg-blue-100:hover{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-50:hover{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-500:hover{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-600:hover{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-green-200:hover{--tw-bg-opacity: 1;background-color:rgb(187 247 208 / var(--tw-bg-opacity, 1))}.hover\:bg-green-500:hover{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.hover\:bg-orange-700:hover{--tw-bg-opacity: 1;background-color:rgb(194 65 12 / var(--tw-bg-opacity, 1))}.hover\:bg-slate-100:hover{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.hover\:bg-slate-200:hover{--tw-bg-opacity: 1;background-color:rgb(226 232 240 / var(--tw-bg-opacity, 1))}.hover\:bg-slate-300:hover{--tw-bg-opacity: 1;background-color:rgb(203 213 225 / var(--tw-bg-opacity, 1))}.hover\:bg-slate-50:hover{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity, 1))}.hover\:bg-slate-50\/50:hover{background-color:#f8fafc80}.hover\:bg-slate-800\/50:hover{background-color:#1e293b80}.hover\:bg-white:hover{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.hover\:text-blue-500:hover{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.hover\:text-blue-600:hover{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.hover\:text-blue-800:hover{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.hover\:text-green-700:hover{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.hover\:text-orange-600:hover{--tw-text-opacity: 1;color:rgb(234 88 12 / var(--tw-text-opacity, 1))}.hover\:text-red-500:hover{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.hover\:text-red-600:hover{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.hover\:text-red-700:hover{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.hover\:text-slate-800:hover{--tw-text-opacity: 1;color:rgb(30 41 59 / var(--tw-text-opacity, 1))}.hover\:text-slate-900:hover{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-lg:hover{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.focus\:ring-blue-600:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(37 99 235 / var(--tw-ring-opacity, 1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-slate-400:disabled{--tw-bg-opacity: 1;background-color:rgb(148 163 184 / var(--tw-bg-opacity, 1))}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:shadow-none:disabled{--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.group[open] .group-open\:rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:bg-slate-50{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity, 1))}.group\/row:hover .group-hover\/row\:opacity-100,.group:hover .group-hover\:opacity-100{opacity:1}.dark\:divide-slate-800:is(.dark *)>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(30 41 59 / var(--tw-divide-opacity, 1))}.dark\:divide-slate-800\/50:is(.dark *)>:not([hidden])~:not([hidden]){border-color:#1e293b80}.dark\:border-blue-700:is(.dark *){--tw-border-opacity: 1;border-color:rgb(29 78 216 / var(--tw-border-opacity, 1))}.dark\:border-blue-900\/50:is(.dark *){border-color:#1e3a8a80}.dark\:border-green-900\/30:is(.dark *){border-color:#14532d4d}.dark\:border-orange-800:is(.dark *){--tw-border-opacity: 1;border-color:rgb(154 52 18 / var(--tw-border-opacity, 1))}.dark\:border-orange-800\/50:is(.dark *){border-color:#9a341280}.dark\:border-orange-900\/50:is(.dark *){border-color:#7c2d1280}.dark\:border-purple-800:is(.dark *){--tw-border-opacity: 1;border-color:rgb(107 33 168 / var(--tw-border-opacity, 1))}.dark\:border-purple-900\/50:is(.dark *){border-color:#581c8780}.dark\:border-red-900\/30:is(.dark *){border-color:#7f1d1d4d}.dark\:border-slate-700:is(.dark *){--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity, 1))}.dark\:border-slate-700\/50:is(.dark *){border-color:#33415580}.dark\:border-slate-800:is(.dark *){--tw-border-opacity: 1;border-color:rgb(30 41 59 / var(--tw-border-opacity, 1))}.dark\:border-slate-800\/50:is(.dark *){border-color:#1e293b80}.dark\:border-slate-900:is(.dark *){--tw-border-opacity: 1;border-color:rgb(15 23 42 / var(--tw-border-opacity, 1))}.dark\:border-yellow-800\/50:is(.dark *){border-color:#854d0e80}.dark\:bg-blue-900\/10:is(.dark *){background-color:#1e3a8a1a}.dark\:bg-blue-900\/30:is(.dark *){background-color:#1e3a8a4d}.dark\:bg-green-900\/10:is(.dark *){background-color:#14532d1a}.dark\:bg-green-900\/30:is(.dark *){background-color:#14532d4d}.dark\:bg-green-900\/50:is(.dark *){background-color:#14532d80}.dark\:bg-orange-900\/10:is(.dark *){background-color:#7c2d121a}.dark\:bg-orange-900\/20:is(.dark *){background-color:#7c2d1233}.dark\:bg-purple-900\/10:is(.dark *){background-color:#581c871a}.dark\:bg-red-900\/10:is(.dark *){background-color:#7f1d1d1a}.dark\:bg-red-900\/50:is(.dark *){background-color:#7f1d1d80}.dark\:bg-slate-700:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity, 1))}.dark\:bg-slate-800:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity, 1))}.dark\:bg-slate-800\/30:is(.dark *){background-color:#1e293b4d}.dark\:bg-slate-800\/50:is(.dark *){background-color:#1e293b80}.dark\:bg-slate-900:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity, 1))}.dark\:bg-slate-900\/20:is(.dark *){background-color:#0f172a33}.dark\:bg-slate-900\/50:is(.dark *){background-color:#0f172a80}.dark\:bg-slate-900\/80:is(.dark *){background-color:#0f172acc}.dark\:bg-slate-950:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(2 6 23 / var(--tw-bg-opacity, 1))}.dark\:bg-slate-950\/30:is(.dark *){background-color:#0206174d}.dark\:bg-slate-950\/50:is(.dark *){background-color:#02061780}.dark\:bg-yellow-900\/30:is(.dark *){background-color:#713f124d}.dark\:bg-yellow-900\/50:is(.dark *){background-color:#713f1280}.dark\:text-blue-400:is(.dark *){--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.dark\:text-blue-400\/80:is(.dark *){color:#60a5facc}.dark\:text-blue-500:is(.dark *){--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.dark\:text-green-300:is(.dark *){--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.dark\:text-green-400:is(.dark *){--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.dark\:text-orange-300:is(.dark *){--tw-text-opacity: 1;color:rgb(253 186 116 / var(--tw-text-opacity, 1))}.dark\:text-orange-400:is(.dark *){--tw-text-opacity: 1;color:rgb(251 146 60 / var(--tw-text-opacity, 1))}.dark\:text-purple-300:is(.dark *){--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity, 1))}.dark\:text-red-300:is(.dark *){--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.dark\:text-red-400:is(.dark *){--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.dark\:text-slate-200:is(.dark *){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}.dark\:text-slate-300:is(.dark *){--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}.dark\:text-slate-400:is(.dark *){--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity, 1))}.dark\:text-slate-600:is(.dark *){--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1))}.dark\:text-white:is(.dark *){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.dark\:text-yellow-300:is(.dark *){--tw-text-opacity: 1;color:rgb(253 224 71 / var(--tw-text-opacity, 1))}.dark\:text-yellow-400:is(.dark *){--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.dark\:text-yellow-500:is(.dark *){--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity, 1))}.dark\:shadow-xl:is(.dark *){--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.dark\:hover\:border-slate-700:hover:is(.dark *){--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity, 1))}.dark\:hover\:bg-blue-900\/20:hover:is(.dark *){background-color:#1e3a8a33}.dark\:hover\:bg-blue-900\/30:hover:is(.dark *){background-color:#1e3a8a4d}.dark\:hover\:bg-green-900:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(20 83 45 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-slate-600:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-slate-700:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-slate-800:hover:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-slate-800\/30:hover:is(.dark *){background-color:#1e293b4d}.dark\:hover\:bg-slate-800\/50:hover:is(.dark *){background-color:#1e293b80}.dark\:hover\:bg-slate-900\/30:hover:is(.dark *){background-color:#0f172a4d}.dark\:hover\:text-blue-300:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.dark\:hover\:text-blue-400:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.dark\:hover\:text-orange-500:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(249 115 22 / var(--tw-text-opacity, 1))}.dark\:hover\:text-red-500:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.dark\:hover\:text-slate-200:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(226 232 240 / var(--tw-text-opacity, 1))}.dark\:hover\:text-slate-300:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(203 213 225 / var(--tw-text-opacity, 1))}.dark\:hover\:text-white:hover:is(.dark *){--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.group:hover .dark\:group-hover\:bg-slate-900:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity, 1))}@media (min-width: 640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pr-0{padding-right:0}}@media (min-width: 768px){.md\:block{display:block}.md\:inline{display:inline}.md\:flex{display:flex}.md\:hidden{display:none}.md\:w-\[600px\]{width:600px}.md\:w-auto{width:auto}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:gap-4{gap:1rem}}@media (min-width: 1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (min-width: 1280px){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}} diff --git a/company-explorer/frontend/dist/assets/index-tQU9lyIc.js b/company-explorer/frontend/dist/assets/index-tQU9lyIc.js new file mode 100644 index 00000000..6dcb9dac --- /dev/null +++ b/company-explorer/frontend/dist/assets/index-tQU9lyIc.js @@ -0,0 +1,309 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))r(s);new MutationObserver(s=>{for(const a of s)if(a.type==="childList")for(const i of a.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(s){const a={};return s.integrity&&(a.integrity=s.integrity),s.referrerPolicy&&(a.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?a.credentials="include":s.crossOrigin==="anonymous"?a.credentials="omit":a.credentials="same-origin",a}function r(s){if(s.ep)return;s.ep=!0;const a=n(s);fetch(s.href,a)}})();function t0(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Vu={exports:{}},tl={},Bu={exports:{}},H={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Ur=Symbol.for("react.element"),n0=Symbol.for("react.portal"),r0=Symbol.for("react.fragment"),s0=Symbol.for("react.strict_mode"),l0=Symbol.for("react.profiler"),a0=Symbol.for("react.provider"),i0=Symbol.for("react.context"),o0=Symbol.for("react.forward_ref"),u0=Symbol.for("react.suspense"),c0=Symbol.for("react.memo"),d0=Symbol.for("react.lazy"),po=Symbol.iterator;function f0(e){return e===null||typeof e!="object"?null:(e=po&&e[po]||e["@@iterator"],typeof e=="function"?e:null)}var Hu={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Wu=Object.assign,Qu={};function Qn(e,t,n){this.props=e,this.context=t,this.refs=Qu,this.updater=n||Hu}Qn.prototype.isReactComponent={};Qn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};Qn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function qu(){}qu.prototype=Qn.prototype;function li(e,t,n){this.props=e,this.context=t,this.refs=Qu,this.updater=n||Hu}var ai=li.prototype=new qu;ai.constructor=li;Wu(ai,Qn.prototype);ai.isPureReactComponent=!0;var ho=Array.isArray,Ku=Object.prototype.hasOwnProperty,ii={current:null},Ju={key:!0,ref:!0,__self:!0,__source:!0};function Gu(e,t,n){var r,s={},a=null,i=null;if(t!=null)for(r in t.ref!==void 0&&(i=t.ref),t.key!==void 0&&(a=""+t.key),t)Ku.call(t,r)&&!Ju.hasOwnProperty(r)&&(s[r]=t[r]);var o=arguments.length-2;if(o===1)s.children=n;else if(1>>1,J=_[Q];if(0>>1;Qs(Ct,A))hts(mt,Ct)?(_[Q]=mt,_[ht]=A,Q=ht):(_[Q]=Ct,_[Ee]=A,Q=Ee);else if(hts(mt,A))_[Q]=mt,_[ht]=A,Q=ht;else break e}}return M}function s(_,M){var A=_.sortIndex-M.sortIndex;return A!==0?A:_.id-M.id}if(typeof performance=="object"&&typeof performance.now=="function"){var a=performance;e.unstable_now=function(){return a.now()}}else{var i=Date,o=i.now();e.unstable_now=function(){return i.now()-o}}var u=[],c=[],f=1,m=null,x=3,y=!1,g=!1,k=!1,S=typeof setTimeout=="function"?setTimeout:null,p=typeof clearTimeout=="function"?clearTimeout:null,d=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function h(_){for(var M=n(c);M!==null;){if(M.callback===null)r(c);else if(M.startTime<=_)r(c),M.sortIndex=M.expirationTime,t(u,M);else break;M=n(c)}}function N(_){if(k=!1,h(_),!g)if(n(u)!==null)g=!0,I(C);else{var M=n(c);M!==null&&V(N,M.startTime-_)}}function C(_,M){g=!1,k&&(k=!1,p(b),b=-1),y=!0;var A=x;try{for(h(M),m=n(u);m!==null&&(!(m.expirationTime>M)||_&&!z());){var Q=m.callback;if(typeof Q=="function"){m.callback=null,x=m.priorityLevel;var J=Q(m.expirationTime<=M);M=e.unstable_now(),typeof J=="function"?m.callback=J:m===n(u)&&r(u),h(M)}else r(u);m=n(u)}if(m!==null)var Ge=!0;else{var Ee=n(c);Ee!==null&&V(N,Ee.startTime-M),Ge=!1}return Ge}finally{m=null,x=A,y=!1}}var L=!1,P=null,b=-1,U=5,w=-1;function z(){return!(e.unstable_now()-w_||125<_?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):U=0<_?Math.floor(1e3/_):5},e.unstable_getCurrentPriorityLevel=function(){return x},e.unstable_getFirstCallbackNode=function(){return n(u)},e.unstable_next=function(_){switch(x){case 1:case 2:case 3:var M=3;break;default:M=x}var A=x;x=M;try{return _()}finally{x=A}},e.unstable_pauseExecution=function(){},e.unstable_requestPaint=function(){},e.unstable_runWithPriority=function(_,M){switch(_){case 1:case 2:case 3:case 4:case 5:break;default:_=3}var A=x;x=_;try{return M()}finally{x=A}},e.unstable_scheduleCallback=function(_,M,A){var Q=e.unstable_now();switch(typeof A=="object"&&A!==null?(A=A.delay,A=typeof A=="number"&&0Q?(_.sortIndex=A,t(c,_),n(u)===null&&_===n(c)&&(k?(p(b),b=-1):k=!0,V(N,A-Q))):(_.sortIndex=J,t(u,_),g||y||(g=!0,I(C))),_},e.unstable_shouldYield=z,e.unstable_wrapCallback=function(_){var M=x;return function(){var A=x;x=M;try{return _.apply(this,arguments)}finally{x=A}}}})(tc);ec.exports=tc;var j0=ec.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var S0=E,$e=j0;function R(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),sa=Object.prototype.hasOwnProperty,C0=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,xo={},go={};function E0(e){return sa.call(go,e)?!0:sa.call(xo,e)?!1:C0.test(e)?go[e]=!0:(xo[e]=!0,!1)}function _0(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function R0(e,t,n,r){if(t===null||typeof t>"u"||_0(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function Ce(e,t,n,r,s,a,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=s,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=a,this.removeEmptyString=i}var xe={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){xe[e]=new Ce(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];xe[t]=new Ce(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){xe[e]=new Ce(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){xe[e]=new Ce(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){xe[e]=new Ce(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){xe[e]=new Ce(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){xe[e]=new Ce(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){xe[e]=new Ce(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){xe[e]=new Ce(e,5,!1,e.toLowerCase(),null,!1,!1)});var ui=/[\-:]([a-z])/g;function ci(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(ui,ci);xe[t]=new Ce(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(ui,ci);xe[t]=new Ce(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(ui,ci);xe[t]=new Ce(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){xe[e]=new Ce(e,1,!1,e.toLowerCase(),null,!1,!1)});xe.xlinkHref=new Ce("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){xe[e]=new Ce(e,1,!1,e.toLowerCase(),null,!0,!0)});function di(e,t,n,r){var s=xe.hasOwnProperty(t)?xe[t]:null;(s!==null?s.type!==0:r||!(2o||s[i]!==a[o]){var u=` +`+s[i].replace(" at new "," at ");return e.displayName&&u.includes("")&&(u=u.replace("",e.displayName)),u}while(1<=i&&0<=o);break}}}finally{El=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?or(e):""}function P0(e){switch(e.tag){case 5:return or(e.type);case 16:return or("Lazy");case 13:return or("Suspense");case 19:return or("SuspenseList");case 0:case 2:case 15:return e=_l(e.type,!1),e;case 11:return e=_l(e.type.render,!1),e;case 1:return e=_l(e.type,!0),e;default:return""}}function oa(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case vn:return"Fragment";case yn:return"Portal";case la:return"Profiler";case fi:return"StrictMode";case aa:return"Suspense";case ia:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case sc:return(e.displayName||"Context")+".Consumer";case rc:return(e._context.displayName||"Context")+".Provider";case pi:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case hi:return t=e.displayName||null,t!==null?t:oa(e.type)||"Memo";case _t:t=e._payload,e=e._init;try{return oa(e(t))}catch{}}return null}function T0(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return oa(t);case 8:return t===fi?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function Bt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function ac(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function L0(e){var t=ac(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var s=n.get,a=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return s.call(this)},set:function(i){r=""+i,a.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Yr(e){e._valueTracker||(e._valueTracker=L0(e))}function ic(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=ac(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Rs(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ua(e,t){var n=t.checked;return se({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function vo(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=Bt(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function oc(e,t){t=t.checked,t!=null&&di(e,"checked",t,!1)}function ca(e,t){oc(e,t);var n=Bt(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?da(e,t.type,n):t.hasOwnProperty("defaultValue")&&da(e,t.type,Bt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function wo(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function da(e,t,n){(t!=="number"||Rs(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var ur=Array.isArray;function Tn(e,t,n,r){if(e=e.options,t){t={};for(var s=0;s"+t.valueOf().toString()+"",t=Zr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Nr(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var pr={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},O0=["Webkit","ms","Moz","O"];Object.keys(pr).forEach(function(e){O0.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),pr[t]=pr[e]})});function fc(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||pr.hasOwnProperty(e)&&pr[e]?(""+t).trim():t+"px"}function pc(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,s=fc(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,s):e[n]=s}}var M0=se({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function ha(e,t){if(t){if(M0[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(R(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(R(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(R(61))}if(t.style!=null&&typeof t.style!="object")throw Error(R(62))}}function ma(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var xa=null;function mi(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var ga=null,Ln=null,On=null;function No(e){if(e=Br(e)){if(typeof ga!="function")throw Error(R(280));var t=e.stateNode;t&&(t=al(t),ga(e.stateNode,e.type,t))}}function hc(e){Ln?On?On.push(e):On=[e]:Ln=e}function mc(){if(Ln){var e=Ln,t=On;if(On=Ln=null,No(e),t)for(e=0;e>>=0,e===0?32:31-(W0(e)/Q0|0)|0}var es=64,ts=4194304;function cr(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Os(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,s=e.suspendedLanes,a=e.pingedLanes,i=n&268435455;if(i!==0){var o=i&~s;o!==0?r=cr(o):(a&=i,a!==0&&(r=cr(a)))}else i=n&~s,i!==0?r=cr(i):a!==0&&(r=cr(a));if(r===0)return 0;if(t!==0&&t!==r&&!(t&s)&&(s=r&-r,a=t&-t,s>=a||s===16&&(a&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function $r(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-st(t),e[t]=n}function G0(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=mr),Lo=" ",Oo=!1;function Dc(e,t){switch(e){case"keyup":return jp.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function zc(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var wn=!1;function Cp(e,t){switch(e){case"compositionend":return zc(t);case"keypress":return t.which!==32?null:(Oo=!0,Lo);case"textInput":return e=t.data,e===Lo&&Oo?null:e;default:return null}}function Ep(e,t){if(wn)return e==="compositionend"||!Ni&&Dc(e,t)?(e=Oc(),gs=wi=Ot=null,wn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=Ao(n)}}function Uc(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Uc(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function $c(){for(var e=window,t=Rs();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Rs(e.document)}return t}function ji(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function zp(e){var t=$c(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Uc(n.ownerDocument.documentElement,n)){if(r!==null&&ji(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var s=n.textContent.length,a=Math.min(r.start,s);r=r.end===void 0?a:Math.min(r.end,s),!e.extend&&a>r&&(s=r,r=a,a=s),s=Fo(n,a);var i=Fo(n,r);s&&i&&(e.rangeCount!==1||e.anchorNode!==s.node||e.anchorOffset!==s.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&(t=t.createRange(),t.setStart(s.node,s.offset),e.removeAllRanges(),a>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,kn=null,Na=null,gr=null,ja=!1;function Io(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;ja||kn==null||kn!==Rs(r)||(r=kn,"selectionStart"in r&&ji(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),gr&&Rr(gr,r)||(gr=r,r=zs(Na,"onSelect"),0jn||(e.current=Pa[jn],Pa[jn]=null,jn--)}function X(e,t){jn++,Pa[jn]=e.current,e.current=t}var Ht={},ke=qt(Ht),Te=qt(!1),an=Ht;function In(e,t){var n=e.type.contextTypes;if(!n)return Ht;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var s={},a;for(a in n)s[a]=t[a];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=s),s}function Le(e){return e=e.childContextTypes,e!=null}function Fs(){ee(Te),ee(ke)}function Qo(e,t,n){if(ke.current!==Ht)throw Error(R(168));X(ke,t),X(Te,n)}function Gc(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var s in r)if(!(s in t))throw Error(R(108,T0(e)||"Unknown",s));return se({},n,r)}function Is(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Ht,an=ke.current,X(ke,e),X(Te,Te.current),!0}function qo(e,t,n){var r=e.stateNode;if(!r)throw Error(R(169));n?(e=Gc(e,t,an),r.__reactInternalMemoizedMergedChildContext=e,ee(Te),ee(ke),X(ke,e)):ee(Te),X(Te,n)}var gt=null,il=!1,Vl=!1;function Xc(e){gt===null?gt=[e]:gt.push(e)}function Kp(e){il=!0,Xc(e)}function Kt(){if(!Vl&>!==null){Vl=!0;var e=0,t=K;try{var n=gt;for(K=1;e>=i,s-=i,yt=1<<32-st(t)+s|n<b?(U=P,P=null):U=P.sibling;var w=x(p,P,h[b],N);if(w===null){P===null&&(P=U);break}e&&P&&w.alternate===null&&t(p,P),d=a(w,d,b),L===null?C=w:L.sibling=w,L=w,P=U}if(b===h.length)return n(p,P),te&&Xt(p,b),C;if(P===null){for(;bb?(U=P,P=null):U=P.sibling;var z=x(p,P,w.value,N);if(z===null){P===null&&(P=U);break}e&&P&&z.alternate===null&&t(p,P),d=a(z,d,b),L===null?C=z:L.sibling=z,L=z,P=U}if(w.done)return n(p,P),te&&Xt(p,b),C;if(P===null){for(;!w.done;b++,w=h.next())w=m(p,w.value,N),w!==null&&(d=a(w,d,b),L===null?C=w:L.sibling=w,L=w);return te&&Xt(p,b),C}for(P=r(p,P);!w.done;b++,w=h.next())w=y(P,p,b,w.value,N),w!==null&&(e&&w.alternate!==null&&P.delete(w.key===null?b:w.key),d=a(w,d,b),L===null?C=w:L.sibling=w,L=w);return e&&P.forEach(function(W){return t(p,W)}),te&&Xt(p,b),C}function S(p,d,h,N){if(typeof h=="object"&&h!==null&&h.type===vn&&h.key===null&&(h=h.props.children),typeof h=="object"&&h!==null){switch(h.$$typeof){case Xr:e:{for(var C=h.key,L=d;L!==null;){if(L.key===C){if(C=h.type,C===vn){if(L.tag===7){n(p,L.sibling),d=s(L,h.props.children),d.return=p,p=d;break e}}else if(L.elementType===C||typeof C=="object"&&C!==null&&C.$$typeof===_t&&Go(C)===L.type){n(p,L.sibling),d=s(L,h.props),d.ref=sr(p,L,h),d.return=p,p=d;break e}n(p,L);break}else t(p,L);L=L.sibling}h.type===vn?(d=sn(h.props.children,p.mode,N,h.key),d.return=p,p=d):(N=Ss(h.type,h.key,h.props,null,p.mode,N),N.ref=sr(p,d,h),N.return=p,p=N)}return i(p);case yn:e:{for(L=h.key;d!==null;){if(d.key===L)if(d.tag===4&&d.stateNode.containerInfo===h.containerInfo&&d.stateNode.implementation===h.implementation){n(p,d.sibling),d=s(d,h.children||[]),d.return=p,p=d;break e}else{n(p,d);break}else t(p,d);d=d.sibling}d=Gl(h,p.mode,N),d.return=p,p=d}return i(p);case _t:return L=h._init,S(p,d,L(h._payload),N)}if(ur(h))return g(p,d,h,N);if(Zn(h))return k(p,d,h,N);os(p,h)}return typeof h=="string"&&h!==""||typeof h=="number"?(h=""+h,d!==null&&d.tag===6?(n(p,d.sibling),d=s(d,h),d.return=p,p=d):(n(p,d),d=Jl(h,p.mode,N),d.return=p,p=d),i(p)):n(p,d)}return S}var $n=td(!0),nd=td(!1),Vs=qt(null),Bs=null,En=null,_i=null;function Ri(){_i=En=Bs=null}function Pi(e){var t=Vs.current;ee(Vs),e._currentValue=t}function Oa(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function Dn(e,t){Bs=e,_i=En=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(Pe=!0),e.firstContext=null)}function Ke(e){var t=e._currentValue;if(_i!==e)if(e={context:e,memoizedValue:t,next:null},En===null){if(Bs===null)throw Error(R(308));En=e,Bs.dependencies={lanes:0,firstContext:e}}else En=En.next=e;return t}var en=null;function Ti(e){en===null?en=[e]:en.push(e)}function rd(e,t,n,r){var s=t.interleaved;return s===null?(n.next=n,Ti(t)):(n.next=s.next,s.next=n),t.interleaved=n,Nt(e,r)}function Nt(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var Rt=!1;function Li(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function sd(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function wt(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function It(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,q&2){var s=r.pending;return s===null?t.next=t:(t.next=s.next,s.next=t),r.pending=t,Nt(e,n)}return s=r.interleaved,s===null?(t.next=t,Ti(r)):(t.next=s.next,s.next=t),r.interleaved=t,Nt(e,n)}function vs(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,gi(e,n)}}function Xo(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var s=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var i={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};a===null?s=a=i:a=a.next=i,n=n.next}while(n!==null);a===null?s=a=t:a=a.next=t}else s=a=t;n={baseState:r.baseState,firstBaseUpdate:s,lastBaseUpdate:a,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function Hs(e,t,n,r){var s=e.updateQueue;Rt=!1;var a=s.firstBaseUpdate,i=s.lastBaseUpdate,o=s.shared.pending;if(o!==null){s.shared.pending=null;var u=o,c=u.next;u.next=null,i===null?a=c:i.next=c,i=u;var f=e.alternate;f!==null&&(f=f.updateQueue,o=f.lastBaseUpdate,o!==i&&(o===null?f.firstBaseUpdate=c:o.next=c,f.lastBaseUpdate=u))}if(a!==null){var m=s.baseState;i=0,f=c=u=null,o=a;do{var x=o.lane,y=o.eventTime;if((r&x)===x){f!==null&&(f=f.next={eventTime:y,lane:0,tag:o.tag,payload:o.payload,callback:o.callback,next:null});e:{var g=e,k=o;switch(x=t,y=n,k.tag){case 1:if(g=k.payload,typeof g=="function"){m=g.call(y,m,x);break e}m=g;break e;case 3:g.flags=g.flags&-65537|128;case 0:if(g=k.payload,x=typeof g=="function"?g.call(y,m,x):g,x==null)break e;m=se({},m,x);break e;case 2:Rt=!0}}o.callback!==null&&o.lane!==0&&(e.flags|=64,x=s.effects,x===null?s.effects=[o]:x.push(o))}else y={eventTime:y,lane:x,tag:o.tag,payload:o.payload,callback:o.callback,next:null},f===null?(c=f=y,u=m):f=f.next=y,i|=x;if(o=o.next,o===null){if(o=s.shared.pending,o===null)break;x=o,o=x.next,x.next=null,s.lastBaseUpdate=x,s.shared.pending=null}}while(!0);if(f===null&&(u=m),s.baseState=u,s.firstBaseUpdate=c,s.lastBaseUpdate=f,t=s.shared.interleaved,t!==null){s=t;do i|=s.lane,s=s.next;while(s!==t)}else a===null&&(s.shared.lanes=0);cn|=i,e.lanes=i,e.memoizedState=m}}function Yo(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Hl.transition;Hl.transition={};try{e(!1),t()}finally{K=n,Hl.transition=r}}function kd(){return Je().memoizedState}function Yp(e,t,n){var r=$t(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},bd(e))Nd(t,n);else if(n=rd(e,t,n,r),n!==null){var s=je();lt(n,e,r,s),jd(n,t,r)}}function Zp(e,t,n){var r=$t(e),s={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(bd(e))Nd(t,s);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var i=t.lastRenderedState,o=a(i,n);if(s.hasEagerState=!0,s.eagerState=o,at(o,i)){var u=t.interleaved;u===null?(s.next=s,Ti(t)):(s.next=u.next,u.next=s),t.interleaved=s;return}}catch{}finally{}n=rd(e,t,s,r),n!==null&&(s=je(),lt(n,e,r,s),jd(n,t,r))}}function bd(e){var t=e.alternate;return e===re||t!==null&&t===re}function Nd(e,t){yr=Qs=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function jd(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,gi(e,n)}}var qs={readContext:Ke,useCallback:ge,useContext:ge,useEffect:ge,useImperativeHandle:ge,useInsertionEffect:ge,useLayoutEffect:ge,useMemo:ge,useReducer:ge,useRef:ge,useState:ge,useDebugValue:ge,useDeferredValue:ge,useTransition:ge,useMutableSource:ge,useSyncExternalStore:ge,useId:ge,unstable_isNewReconciler:!1},eh={readContext:Ke,useCallback:function(e,t){return ct().memoizedState=[e,t===void 0?null:t],e},useContext:Ke,useEffect:eu,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,ks(4194308,4,xd.bind(null,t,e),n)},useLayoutEffect:function(e,t){return ks(4194308,4,e,t)},useInsertionEffect:function(e,t){return ks(4,2,e,t)},useMemo:function(e,t){var n=ct();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=ct();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=Yp.bind(null,re,e),[r.memoizedState,e]},useRef:function(e){var t=ct();return e={current:e},t.memoizedState=e},useState:Zo,useDebugValue:Ui,useDeferredValue:function(e){return ct().memoizedState=e},useTransition:function(){var e=Zo(!1),t=e[0];return e=Xp.bind(null,e[1]),ct().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=re,s=ct();if(te){if(n===void 0)throw Error(R(407));n=n()}else{if(n=t(),fe===null)throw Error(R(349));un&30||od(r,t,n)}s.memoizedState=n;var a={value:n,getSnapshot:t};return s.queue=a,eu(cd.bind(null,r,a,e),[e]),r.flags|=2048,Ar(9,ud.bind(null,r,a,n,t),void 0,null),n},useId:function(){var e=ct(),t=fe.identifierPrefix;if(te){var n=vt,r=yt;n=(r&~(1<<32-st(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Dr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=i.createElement(n,{is:r.is}):(e=i.createElement(n),n==="select"&&(i=e,r.multiple?i.multiple=!0:r.size&&(i.size=r.size))):e=i.createElementNS(e,n),e[dt]=t,e[Lr]=r,Md(e,t,!1,!1),t.stateNode=e;e:{switch(i=ma(n,r),n){case"dialog":Z("cancel",e),Z("close",e),s=r;break;case"iframe":case"object":case"embed":Z("load",e),s=r;break;case"video":case"audio":for(s=0;sHn&&(t.flags|=128,r=!0,lr(a,!1),t.lanes=4194304)}else{if(!r)if(e=Ws(i),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),lr(a,!0),a.tail===null&&a.tailMode==="hidden"&&!i.alternate&&!te)return ye(t),null}else 2*ae()-a.renderingStartTime>Hn&&n!==1073741824&&(t.flags|=128,r=!0,lr(a,!1),t.lanes=4194304);a.isBackwards?(i.sibling=t.child,t.child=i):(n=a.last,n!==null?n.sibling=i:t.child=i,a.last=i)}return a.tail!==null?(t=a.tail,a.rendering=t,a.tail=t.sibling,a.renderingStartTime=ae(),t.sibling=null,n=ne.current,X(ne,r?n&1|2:n&1),t):(ye(t),null);case 22:case 23:return Qi(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?Fe&1073741824&&(ye(t),t.subtreeFlags&6&&(t.flags|=8192)):ye(t),null;case 24:return null;case 25:return null}throw Error(R(156,t.tag))}function oh(e,t){switch(Ci(t),t.tag){case 1:return Le(t.type)&&Fs(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Vn(),ee(Te),ee(ke),Di(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Mi(t),null;case 13:if(ee(ne),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(R(340));Un()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return ee(ne),null;case 4:return Vn(),null;case 10:return Pi(t.type._context),null;case 22:case 23:return Qi(),null;case 24:return null;default:return null}}var cs=!1,ve=!1,uh=typeof WeakSet=="function"?WeakSet:Set,O=null;function _n(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){le(e,t,r)}else n.current=null}function Va(e,t,n){try{n()}catch(r){le(e,t,r)}}var du=!1;function ch(e,t){if(Sa=Ms,e=$c(),ji(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var s=r.anchorOffset,a=r.focusNode;r=r.focusOffset;try{n.nodeType,a.nodeType}catch{n=null;break e}var i=0,o=-1,u=-1,c=0,f=0,m=e,x=null;t:for(;;){for(var y;m!==n||s!==0&&m.nodeType!==3||(o=i+s),m!==a||r!==0&&m.nodeType!==3||(u=i+r),m.nodeType===3&&(i+=m.nodeValue.length),(y=m.firstChild)!==null;)x=m,m=y;for(;;){if(m===e)break t;if(x===n&&++c===s&&(o=i),x===a&&++f===r&&(u=i),(y=m.nextSibling)!==null)break;m=x,x=m.parentNode}m=y}n=o===-1||u===-1?null:{start:o,end:u}}else n=null}n=n||{start:0,end:0}}else n=null;for(Ca={focusedElem:e,selectionRange:n},Ms=!1,O=t;O!==null;)if(t=O,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,O=e;else for(;O!==null;){t=O;try{var g=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(g!==null){var k=g.memoizedProps,S=g.memoizedState,p=t.stateNode,d=p.getSnapshotBeforeUpdate(t.elementType===t.type?k:et(t.type,k),S);p.__reactInternalSnapshotBeforeUpdate=d}break;case 3:var h=t.stateNode.containerInfo;h.nodeType===1?h.textContent="":h.nodeType===9&&h.documentElement&&h.removeChild(h.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(R(163))}}catch(N){le(t,t.return,N)}if(e=t.sibling,e!==null){e.return=t.return,O=e;break}O=t.return}return g=du,du=!1,g}function vr(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var s=r=r.next;do{if((s.tag&e)===e){var a=s.destroy;s.destroy=void 0,a!==void 0&&Va(t,n,a)}s=s.next}while(s!==r)}}function cl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Ba(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Ad(e){var t=e.alternate;t!==null&&(e.alternate=null,Ad(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[dt],delete t[Lr],delete t[Ra],delete t[Qp],delete t[qp])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Fd(e){return e.tag===5||e.tag===3||e.tag===4}function fu(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Fd(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Ha(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=As));else if(r!==4&&(e=e.child,e!==null))for(Ha(e,t,n),e=e.sibling;e!==null;)Ha(e,t,n),e=e.sibling}function Wa(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Wa(e,t,n),e=e.sibling;e!==null;)Wa(e,t,n),e=e.sibling}var he=null,tt=!1;function Et(e,t,n){for(n=n.child;n!==null;)Id(e,t,n),n=n.sibling}function Id(e,t,n){if(ft&&typeof ft.onCommitFiberUnmount=="function")try{ft.onCommitFiberUnmount(nl,n)}catch{}switch(n.tag){case 5:ve||_n(n,t);case 6:var r=he,s=tt;he=null,Et(e,t,n),he=r,tt=s,he!==null&&(tt?(e=he,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):he.removeChild(n.stateNode));break;case 18:he!==null&&(tt?(e=he,n=n.stateNode,e.nodeType===8?$l(e.parentNode,n):e.nodeType===1&&$l(e,n),Er(e)):$l(he,n.stateNode));break;case 4:r=he,s=tt,he=n.stateNode.containerInfo,tt=!0,Et(e,t,n),he=r,tt=s;break;case 0:case 11:case 14:case 15:if(!ve&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){s=r=r.next;do{var a=s,i=a.destroy;a=a.tag,i!==void 0&&(a&2||a&4)&&Va(n,t,i),s=s.next}while(s!==r)}Et(e,t,n);break;case 1:if(!ve&&(_n(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(o){le(n,t,o)}Et(e,t,n);break;case 21:Et(e,t,n);break;case 22:n.mode&1?(ve=(r=ve)||n.memoizedState!==null,Et(e,t,n),ve=r):Et(e,t,n);break;default:Et(e,t,n)}}function pu(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new uh),t.forEach(function(r){var s=vh.bind(null,e,r);n.has(r)||(n.add(r),r.then(s,s))})}}function Ze(e,t){var n=t.deletions;if(n!==null)for(var r=0;rs&&(s=i),r&=~a}if(r=s,r=ae()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*fh(r/1960))-r,10e?16:e,Mt===null)var r=!1;else{if(e=Mt,Mt=null,Gs=0,q&6)throw Error(R(331));var s=q;for(q|=4,O=e.current;O!==null;){var a=O,i=a.child;if(O.flags&16){var o=a.deletions;if(o!==null){for(var u=0;uae()-Hi?rn(e,0):Bi|=n),Oe(e,t)}function qd(e,t){t===0&&(e.mode&1?(t=ts,ts<<=1,!(ts&130023424)&&(ts=4194304)):t=1);var n=je();e=Nt(e,t),e!==null&&($r(e,t,n),Oe(e,n))}function yh(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),qd(e,n)}function vh(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,s=e.memoizedState;s!==null&&(n=s.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(R(314))}r!==null&&r.delete(t),qd(e,n)}var Kd;Kd=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Te.current)Pe=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return Pe=!1,ah(e,t,n);Pe=!!(e.flags&131072)}else Pe=!1,te&&t.flags&1048576&&Yc(t,$s,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;bs(e,t),e=t.pendingProps;var s=In(t,ke.current);Dn(t,n),s=Ai(null,t,r,e,s,n);var a=Fi();return t.flags|=1,typeof s=="object"&&s!==null&&typeof s.render=="function"&&s.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Le(r)?(a=!0,Is(t)):a=!1,t.memoizedState=s.state!==null&&s.state!==void 0?s.state:null,Li(t),s.updater=ul,t.stateNode=s,s._reactInternals=t,Da(t,r,e,n),t=Fa(null,t,r,!0,a,n)):(t.tag=0,te&&a&&Si(t),Ne(null,t,s,n),t=t.child),t;case 16:r=t.elementType;e:{switch(bs(e,t),e=t.pendingProps,s=r._init,r=s(r._payload),t.type=r,s=t.tag=kh(r),e=et(r,e),s){case 0:t=Aa(null,t,r,e,n);break e;case 1:t=ou(null,t,r,e,n);break e;case 11:t=au(null,t,r,e,n);break e;case 14:t=iu(null,t,r,et(r.type,e),n);break e}throw Error(R(306,r,""))}return t;case 0:return r=t.type,s=t.pendingProps,s=t.elementType===r?s:et(r,s),Aa(e,t,r,s,n);case 1:return r=t.type,s=t.pendingProps,s=t.elementType===r?s:et(r,s),ou(e,t,r,s,n);case 3:e:{if(Td(t),e===null)throw Error(R(387));r=t.pendingProps,a=t.memoizedState,s=a.element,sd(e,t),Hs(t,r,null,n);var i=t.memoizedState;if(r=i.element,a.isDehydrated)if(a={element:r,isDehydrated:!1,cache:i.cache,pendingSuspenseBoundaries:i.pendingSuspenseBoundaries,transitions:i.transitions},t.updateQueue.baseState=a,t.memoizedState=a,t.flags&256){s=Bn(Error(R(423)),t),t=uu(e,t,r,n,s);break e}else if(r!==s){s=Bn(Error(R(424)),t),t=uu(e,t,r,n,s);break e}else for(Ie=Ft(t.stateNode.containerInfo.firstChild),Ue=t,te=!0,nt=null,n=nd(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Un(),r===s){t=jt(e,t,n);break e}Ne(e,t,r,n)}t=t.child}return t;case 5:return ld(t),e===null&&La(t),r=t.type,s=t.pendingProps,a=e!==null?e.memoizedProps:null,i=s.children,Ea(r,s)?i=null:a!==null&&Ea(r,a)&&(t.flags|=32),Pd(e,t),Ne(e,t,i,n),t.child;case 6:return e===null&&La(t),null;case 13:return Ld(e,t,n);case 4:return Oi(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=$n(t,null,r,n):Ne(e,t,r,n),t.child;case 11:return r=t.type,s=t.pendingProps,s=t.elementType===r?s:et(r,s),au(e,t,r,s,n);case 7:return Ne(e,t,t.pendingProps,n),t.child;case 8:return Ne(e,t,t.pendingProps.children,n),t.child;case 12:return Ne(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,s=t.pendingProps,a=t.memoizedProps,i=s.value,X(Vs,r._currentValue),r._currentValue=i,a!==null)if(at(a.value,i)){if(a.children===s.children&&!Te.current){t=jt(e,t,n);break e}}else for(a=t.child,a!==null&&(a.return=t);a!==null;){var o=a.dependencies;if(o!==null){i=a.child;for(var u=o.firstContext;u!==null;){if(u.context===r){if(a.tag===1){u=wt(-1,n&-n),u.tag=2;var c=a.updateQueue;if(c!==null){c=c.shared;var f=c.pending;f===null?u.next=u:(u.next=f.next,f.next=u),c.pending=u}}a.lanes|=n,u=a.alternate,u!==null&&(u.lanes|=n),Oa(a.return,n,t),o.lanes|=n;break}u=u.next}}else if(a.tag===10)i=a.type===t.type?null:a.child;else if(a.tag===18){if(i=a.return,i===null)throw Error(R(341));i.lanes|=n,o=i.alternate,o!==null&&(o.lanes|=n),Oa(i,n,t),i=a.sibling}else i=a.child;if(i!==null)i.return=a;else for(i=a;i!==null;){if(i===t){i=null;break}if(a=i.sibling,a!==null){a.return=i.return,i=a;break}i=i.return}a=i}Ne(e,t,s.children,n),t=t.child}return t;case 9:return s=t.type,r=t.pendingProps.children,Dn(t,n),s=Ke(s),r=r(s),t.flags|=1,Ne(e,t,r,n),t.child;case 14:return r=t.type,s=et(r,t.pendingProps),s=et(r.type,s),iu(e,t,r,s,n);case 15:return _d(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,s=t.pendingProps,s=t.elementType===r?s:et(r,s),bs(e,t),t.tag=1,Le(r)?(e=!0,Is(t)):e=!1,Dn(t,n),Sd(t,r,s),Da(t,r,s,n),Fa(null,t,r,!0,e,n);case 19:return Od(e,t,n);case 22:return Rd(e,t,n)}throw Error(R(156,t.tag))};function Jd(e,t){return bc(e,t)}function wh(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Qe(e,t,n,r){return new wh(e,t,n,r)}function Ki(e){return e=e.prototype,!(!e||!e.isReactComponent)}function kh(e){if(typeof e=="function")return Ki(e)?1:0;if(e!=null){if(e=e.$$typeof,e===pi)return 11;if(e===hi)return 14}return 2}function Vt(e,t){var n=e.alternate;return n===null?(n=Qe(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Ss(e,t,n,r,s,a){var i=2;if(r=e,typeof e=="function")Ki(e)&&(i=1);else if(typeof e=="string")i=5;else e:switch(e){case vn:return sn(n.children,s,a,t);case fi:i=8,s|=8;break;case la:return e=Qe(12,n,t,s|2),e.elementType=la,e.lanes=a,e;case aa:return e=Qe(13,n,t,s),e.elementType=aa,e.lanes=a,e;case ia:return e=Qe(19,n,t,s),e.elementType=ia,e.lanes=a,e;case lc:return fl(n,s,a,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case rc:i=10;break e;case sc:i=9;break e;case pi:i=11;break e;case hi:i=14;break e;case _t:i=16,r=null;break e}throw Error(R(130,e==null?e:typeof e,""))}return t=Qe(i,n,t,s),t.elementType=e,t.type=r,t.lanes=a,t}function sn(e,t,n,r){return e=Qe(7,e,r,t),e.lanes=n,e}function fl(e,t,n,r){return e=Qe(22,e,r,t),e.elementType=lc,e.lanes=n,e.stateNode={isHidden:!1},e}function Jl(e,t,n){return e=Qe(6,e,null,t),e.lanes=n,e}function Gl(e,t,n){return t=Qe(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function bh(e,t,n,r,s){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Pl(0),this.expirationTimes=Pl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Pl(0),this.identifierPrefix=r,this.onRecoverableError=s,this.mutableSourceEagerHydrationData=null}function Ji(e,t,n,r,s,a,i,o,u){return e=new bh(e,t,n,o,u),t===1?(t=1,a===!0&&(t|=8)):t=0,a=Qe(3,null,null,t),e.current=a,a.stateNode=e,a.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Li(a),e}function Nh(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Zd)}catch(e){console.error(e)}}Zd(),Zu.exports=Ve;var _h=Zu.exports,ku=_h;ra.createRoot=ku.createRoot,ra.hydrateRoot=ku.hydrateRoot;function ef(e,t){return function(){return e.apply(t,arguments)}}const{toString:Rh}=Object.prototype,{getPrototypeOf:Zi}=Object,{iterator:gl,toStringTag:tf}=Symbol,yl=(e=>t=>{const n=Rh.call(t);return e[n]||(e[n]=n.slice(8,-1).toLowerCase())})(Object.create(null)),it=e=>(e=e.toLowerCase(),t=>yl(t)===e),vl=e=>t=>typeof t===e,{isArray:Jn}=Array,Wn=vl("undefined");function Wr(e){return e!==null&&!Wn(e)&&e.constructor!==null&&!Wn(e.constructor)&&Me(e.constructor.isBuffer)&&e.constructor.isBuffer(e)}const nf=it("ArrayBuffer");function Ph(e){let t;return typeof ArrayBuffer<"u"&&ArrayBuffer.isView?t=ArrayBuffer.isView(e):t=e&&e.buffer&&nf(e.buffer),t}const Th=vl("string"),Me=vl("function"),rf=vl("number"),Qr=e=>e!==null&&typeof e=="object",Lh=e=>e===!0||e===!1,Cs=e=>{if(yl(e)!=="object")return!1;const t=Zi(e);return(t===null||t===Object.prototype||Object.getPrototypeOf(t)===null)&&!(tf in e)&&!(gl in e)},Oh=e=>{if(!Qr(e)||Wr(e))return!1;try{return Object.keys(e).length===0&&Object.getPrototypeOf(e)===Object.prototype}catch{return!1}},Mh=it("Date"),Dh=it("File"),zh=it("Blob"),Ah=it("FileList"),Fh=e=>Qr(e)&&Me(e.pipe),Ih=e=>{let t;return e&&(typeof FormData=="function"&&e instanceof FormData||Me(e.append)&&((t=yl(e))==="formdata"||t==="object"&&Me(e.toString)&&e.toString()==="[object FormData]"))},Uh=it("URLSearchParams"),[$h,Vh,Bh,Hh]=["ReadableStream","Request","Response","Headers"].map(it),Wh=e=>e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"");function qr(e,t,{allOwnKeys:n=!1}={}){if(e===null||typeof e>"u")return;let r,s;if(typeof e!="object"&&(e=[e]),Jn(e))for(r=0,s=e.length;r0;)if(s=n[r],t===s.toLowerCase())return s;return null}const nn=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:global,lf=e=>!Wn(e)&&e!==nn;function Ga(){const{caseless:e,skipUndefined:t}=lf(this)&&this||{},n={},r=(s,a)=>{const i=e&&sf(n,a)||a;Cs(n[i])&&Cs(s)?n[i]=Ga(n[i],s):Cs(s)?n[i]=Ga({},s):Jn(s)?n[i]=s.slice():(!t||!Wn(s))&&(n[i]=s)};for(let s=0,a=arguments.length;s(qr(t,(s,a)=>{n&&Me(s)?e[a]=ef(s,n):e[a]=s},{allOwnKeys:r}),e),qh=e=>(e.charCodeAt(0)===65279&&(e=e.slice(1)),e),Kh=(e,t,n,r)=>{e.prototype=Object.create(t.prototype,r),e.prototype.constructor=e,Object.defineProperty(e,"super",{value:t.prototype}),n&&Object.assign(e.prototype,n)},Jh=(e,t,n,r)=>{let s,a,i;const o={};if(t=t||{},e==null)return t;do{for(s=Object.getOwnPropertyNames(e),a=s.length;a-- >0;)i=s[a],(!r||r(i,e,t))&&!o[i]&&(t[i]=e[i],o[i]=!0);e=n!==!1&&Zi(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},Gh=(e,t,n)=>{e=String(e),(n===void 0||n>e.length)&&(n=e.length),n-=t.length;const r=e.indexOf(t,n);return r!==-1&&r===n},Xh=e=>{if(!e)return null;if(Jn(e))return e;let t=e.length;if(!rf(t))return null;const n=new Array(t);for(;t-- >0;)n[t]=e[t];return n},Yh=(e=>t=>e&&t instanceof e)(typeof Uint8Array<"u"&&Zi(Uint8Array)),Zh=(e,t)=>{const r=(e&&e[gl]).call(e);let s;for(;(s=r.next())&&!s.done;){const a=s.value;t.call(e,a[0],a[1])}},em=(e,t)=>{let n;const r=[];for(;(n=e.exec(t))!==null;)r.push(n);return r},tm=it("HTMLFormElement"),nm=e=>e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,function(n,r,s){return r.toUpperCase()+s}),bu=(({hasOwnProperty:e})=>(t,n)=>e.call(t,n))(Object.prototype),rm=it("RegExp"),af=(e,t)=>{const n=Object.getOwnPropertyDescriptors(e),r={};qr(n,(s,a)=>{let i;(i=t(s,a,e))!==!1&&(r[a]=i||s)}),Object.defineProperties(e,r)},sm=e=>{af(e,(t,n)=>{if(Me(e)&&["arguments","caller","callee"].indexOf(n)!==-1)return!1;const r=e[n];if(Me(r)){if(t.enumerable=!1,"writable"in t){t.writable=!1;return}t.set||(t.set=()=>{throw Error("Can not rewrite read-only method '"+n+"'")})}})},lm=(e,t)=>{const n={},r=s=>{s.forEach(a=>{n[a]=!0})};return Jn(e)?r(e):r(String(e).split(t)),n},am=()=>{},im=(e,t)=>e!=null&&Number.isFinite(e=+e)?e:t;function om(e){return!!(e&&Me(e.append)&&e[tf]==="FormData"&&e[gl])}const um=e=>{const t=new Array(10),n=(r,s)=>{if(Qr(r)){if(t.indexOf(r)>=0)return;if(Wr(r))return r;if(!("toJSON"in r)){t[s]=r;const a=Jn(r)?[]:{};return qr(r,(i,o)=>{const u=n(i,s+1);!Wn(u)&&(a[o]=u)}),t[s]=void 0,a}}return r};return n(e,0)},cm=it("AsyncFunction"),dm=e=>e&&(Qr(e)||Me(e))&&Me(e.then)&&Me(e.catch),of=((e,t)=>e?setImmediate:t?((n,r)=>(nn.addEventListener("message",({source:s,data:a})=>{s===nn&&a===n&&r.length&&r.shift()()},!1),s=>{r.push(s),nn.postMessage(n,"*")}))(`axios@${Math.random()}`,[]):n=>setTimeout(n))(typeof setImmediate=="function",Me(nn.postMessage)),fm=typeof queueMicrotask<"u"?queueMicrotask.bind(nn):typeof process<"u"&&process.nextTick||of,pm=e=>e!=null&&Me(e[gl]),v={isArray:Jn,isArrayBuffer:nf,isBuffer:Wr,isFormData:Ih,isArrayBufferView:Ph,isString:Th,isNumber:rf,isBoolean:Lh,isObject:Qr,isPlainObject:Cs,isEmptyObject:Oh,isReadableStream:$h,isRequest:Vh,isResponse:Bh,isHeaders:Hh,isUndefined:Wn,isDate:Mh,isFile:Dh,isBlob:zh,isRegExp:rm,isFunction:Me,isStream:Fh,isURLSearchParams:Uh,isTypedArray:Yh,isFileList:Ah,forEach:qr,merge:Ga,extend:Qh,trim:Wh,stripBOM:qh,inherits:Kh,toFlatObject:Jh,kindOf:yl,kindOfTest:it,endsWith:Gh,toArray:Xh,forEachEntry:Zh,matchAll:em,isHTMLForm:tm,hasOwnProperty:bu,hasOwnProp:bu,reduceDescriptors:af,freezeMethods:sm,toObjectSet:lm,toCamelCase:nm,noop:am,toFiniteNumber:im,findKey:sf,global:nn,isContextDefined:lf,isSpecCompliantForm:om,toJSONObject:um,isAsyncFn:cm,isThenable:dm,setImmediate:of,asap:fm,isIterable:pm};function $(e,t,n,r,s){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error().stack,this.message=e,this.name="AxiosError",t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),s&&(this.response=s,this.status=s.status?s.status:null)}v.inherits($,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:v.toJSONObject(this.config),code:this.code,status:this.status}}});const uf=$.prototype,cf={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach(e=>{cf[e]={value:e}});Object.defineProperties($,cf);Object.defineProperty(uf,"isAxiosError",{value:!0});$.from=(e,t,n,r,s,a)=>{const i=Object.create(uf);v.toFlatObject(e,i,function(f){return f!==Error.prototype},c=>c!=="isAxiosError");const o=e&&e.message?e.message:"Error",u=t==null&&e?e.code:t;return $.call(i,o,u,n,r,s),e&&i.cause==null&&Object.defineProperty(i,"cause",{value:e,configurable:!0}),i.name=e&&e.name||"Error",a&&Object.assign(i,a),i};const hm=null;function Xa(e){return v.isPlainObject(e)||v.isArray(e)}function df(e){return v.endsWith(e,"[]")?e.slice(0,-2):e}function Nu(e,t,n){return e?e.concat(t).map(function(s,a){return s=df(s),!n&&a?"["+s+"]":s}).join(n?".":""):t}function mm(e){return v.isArray(e)&&!e.some(Xa)}const xm=v.toFlatObject(v,{},null,function(t){return/^is[A-Z]/.test(t)});function wl(e,t,n){if(!v.isObject(e))throw new TypeError("target must be an object");t=t||new FormData,n=v.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,function(k,S){return!v.isUndefined(S[k])});const r=n.metaTokens,s=n.visitor||f,a=n.dots,i=n.indexes,u=(n.Blob||typeof Blob<"u"&&Blob)&&v.isSpecCompliantForm(t);if(!v.isFunction(s))throw new TypeError("visitor must be a function");function c(g){if(g===null)return"";if(v.isDate(g))return g.toISOString();if(v.isBoolean(g))return g.toString();if(!u&&v.isBlob(g))throw new $("Blob is not supported. Use a Buffer instead.");return v.isArrayBuffer(g)||v.isTypedArray(g)?u&&typeof Blob=="function"?new Blob([g]):Buffer.from(g):g}function f(g,k,S){let p=g;if(g&&!S&&typeof g=="object"){if(v.endsWith(k,"{}"))k=r?k:k.slice(0,-2),g=JSON.stringify(g);else if(v.isArray(g)&&mm(g)||(v.isFileList(g)||v.endsWith(k,"[]"))&&(p=v.toArray(g)))return k=df(k),p.forEach(function(h,N){!(v.isUndefined(h)||h===null)&&t.append(i===!0?Nu([k],N,a):i===null?k:k+"[]",c(h))}),!1}return Xa(g)?!0:(t.append(Nu(S,k,a),c(g)),!1)}const m=[],x=Object.assign(xm,{defaultVisitor:f,convertValue:c,isVisitable:Xa});function y(g,k){if(!v.isUndefined(g)){if(m.indexOf(g)!==-1)throw Error("Circular reference detected in "+k.join("."));m.push(g),v.forEach(g,function(p,d){(!(v.isUndefined(p)||p===null)&&s.call(t,p,v.isString(d)?d.trim():d,k,x))===!0&&y(p,k?k.concat(d):[d])}),m.pop()}}if(!v.isObject(e))throw new TypeError("data must be an object");return y(e),t}function ju(e){const t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,function(r){return t[r]})}function eo(e,t){this._pairs=[],e&&wl(e,this,t)}const ff=eo.prototype;ff.append=function(t,n){this._pairs.push([t,n])};ff.toString=function(t){const n=t?function(r){return t.call(this,r,ju)}:ju;return this._pairs.map(function(s){return n(s[0])+"="+n(s[1])},"").join("&")};function gm(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+")}function pf(e,t,n){if(!t)return e;const r=n&&n.encode||gm;v.isFunction(n)&&(n={serialize:n});const s=n&&n.serialize;let a;if(s?a=s(t,n):a=v.isURLSearchParams(t)?t.toString():new eo(t,n).toString(r),a){const i=e.indexOf("#");i!==-1&&(e=e.slice(0,i)),e+=(e.indexOf("?")===-1?"?":"&")+a}return e}class Su{constructor(){this.handlers=[]}use(t,n,r){return this.handlers.push({fulfilled:t,rejected:n,synchronous:r?r.synchronous:!1,runWhen:r?r.runWhen:null}),this.handlers.length-1}eject(t){this.handlers[t]&&(this.handlers[t]=null)}clear(){this.handlers&&(this.handlers=[])}forEach(t){v.forEach(this.handlers,function(r){r!==null&&t(r)})}}const hf={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},ym=typeof URLSearchParams<"u"?URLSearchParams:eo,vm=typeof FormData<"u"?FormData:null,wm=typeof Blob<"u"?Blob:null,km={isBrowser:!0,classes:{URLSearchParams:ym,FormData:vm,Blob:wm},protocols:["http","https","file","blob","url","data"]},to=typeof window<"u"&&typeof document<"u",Ya=typeof navigator=="object"&&navigator||void 0,bm=to&&(!Ya||["ReactNative","NativeScript","NS"].indexOf(Ya.product)<0),Nm=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope&&typeof self.importScripts=="function",jm=to&&window.location.href||"http://localhost",Sm=Object.freeze(Object.defineProperty({__proto__:null,hasBrowserEnv:to,hasStandardBrowserEnv:bm,hasStandardBrowserWebWorkerEnv:Nm,navigator:Ya,origin:jm},Symbol.toStringTag,{value:"Module"})),we={...Sm,...km};function Cm(e,t){return wl(e,new we.classes.URLSearchParams,{visitor:function(n,r,s,a){return we.isNode&&v.isBuffer(n)?(this.append(r,n.toString("base64")),!1):a.defaultVisitor.apply(this,arguments)},...t})}function Em(e){return v.matchAll(/\w+|\[(\w*)]/g,e).map(t=>t[0]==="[]"?"":t[1]||t[0])}function _m(e){const t={},n=Object.keys(e);let r;const s=n.length;let a;for(r=0;r=n.length;return i=!i&&v.isArray(s)?s.length:i,u?(v.hasOwnProp(s,i)?s[i]=[s[i],r]:s[i]=r,!o):((!s[i]||!v.isObject(s[i]))&&(s[i]=[]),t(n,r,s[i],a)&&v.isArray(s[i])&&(s[i]=_m(s[i])),!o)}if(v.isFormData(e)&&v.isFunction(e.entries)){const n={};return v.forEachEntry(e,(r,s)=>{t(Em(r),s,n,0)}),n}return null}function Rm(e,t,n){if(v.isString(e))try{return(t||JSON.parse)(e),v.trim(e)}catch(r){if(r.name!=="SyntaxError")throw r}return(n||JSON.stringify)(e)}const Kr={transitional:hf,adapter:["xhr","http","fetch"],transformRequest:[function(t,n){const r=n.getContentType()||"",s=r.indexOf("application/json")>-1,a=v.isObject(t);if(a&&v.isHTMLForm(t)&&(t=new FormData(t)),v.isFormData(t))return s?JSON.stringify(mf(t)):t;if(v.isArrayBuffer(t)||v.isBuffer(t)||v.isStream(t)||v.isFile(t)||v.isBlob(t)||v.isReadableStream(t))return t;if(v.isArrayBufferView(t))return t.buffer;if(v.isURLSearchParams(t))return n.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),t.toString();let o;if(a){if(r.indexOf("application/x-www-form-urlencoded")>-1)return Cm(t,this.formSerializer).toString();if((o=v.isFileList(t))||r.indexOf("multipart/form-data")>-1){const u=this.env&&this.env.FormData;return wl(o?{"files[]":t}:t,u&&new u,this.formSerializer)}}return a||s?(n.setContentType("application/json",!1),Rm(t)):t}],transformResponse:[function(t){const n=this.transitional||Kr.transitional,r=n&&n.forcedJSONParsing,s=this.responseType==="json";if(v.isResponse(t)||v.isReadableStream(t))return t;if(t&&v.isString(t)&&(r&&!this.responseType||s)){const i=!(n&&n.silentJSONParsing)&&s;try{return JSON.parse(t,this.parseReviver)}catch(o){if(i)throw o.name==="SyntaxError"?$.from(o,$.ERR_BAD_RESPONSE,this,null,this.response):o}}return t}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:we.classes.FormData,Blob:we.classes.Blob},validateStatus:function(t){return t>=200&&t<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};v.forEach(["delete","get","head","post","put","patch"],e=>{Kr.headers[e]={}});const Pm=v.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),Tm=e=>{const t={};let n,r,s;return e&&e.split(` +`).forEach(function(i){s=i.indexOf(":"),n=i.substring(0,s).trim().toLowerCase(),r=i.substring(s+1).trim(),!(!n||t[n]&&Pm[n])&&(n==="set-cookie"?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+", "+r:r)}),t},Cu=Symbol("internals");function ir(e){return e&&String(e).trim().toLowerCase()}function Es(e){return e===!1||e==null?e:v.isArray(e)?e.map(Es):String(e)}function Lm(e){const t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let r;for(;r=n.exec(e);)t[r[1]]=r[2];return t}const Om=e=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim());function Xl(e,t,n,r,s){if(v.isFunction(r))return r.call(this,t,n);if(s&&(t=n),!!v.isString(t)){if(v.isString(r))return t.indexOf(r)!==-1;if(v.isRegExp(r))return r.test(t)}}function Mm(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(t,n,r)=>n.toUpperCase()+r)}function Dm(e,t){const n=v.toCamelCase(" "+t);["get","set","has"].forEach(r=>{Object.defineProperty(e,r+n,{value:function(s,a,i){return this[r].call(this,t,s,a,i)},configurable:!0})})}let De=class{constructor(t){t&&this.set(t)}set(t,n,r){const s=this;function a(o,u,c){const f=ir(u);if(!f)throw new Error("header name must be a non-empty string");const m=v.findKey(s,f);(!m||s[m]===void 0||c===!0||c===void 0&&s[m]!==!1)&&(s[m||u]=Es(o))}const i=(o,u)=>v.forEach(o,(c,f)=>a(c,f,u));if(v.isPlainObject(t)||t instanceof this.constructor)i(t,n);else if(v.isString(t)&&(t=t.trim())&&!Om(t))i(Tm(t),n);else if(v.isObject(t)&&v.isIterable(t)){let o={},u,c;for(const f of t){if(!v.isArray(f))throw TypeError("Object iterator must return a key-value pair");o[c=f[0]]=(u=o[c])?v.isArray(u)?[...u,f[1]]:[u,f[1]]:f[1]}i(o,n)}else t!=null&&a(n,t,r);return this}get(t,n){if(t=ir(t),t){const r=v.findKey(this,t);if(r){const s=this[r];if(!n)return s;if(n===!0)return Lm(s);if(v.isFunction(n))return n.call(this,s,r);if(v.isRegExp(n))return n.exec(s);throw new TypeError("parser must be boolean|regexp|function")}}}has(t,n){if(t=ir(t),t){const r=v.findKey(this,t);return!!(r&&this[r]!==void 0&&(!n||Xl(this,this[r],r,n)))}return!1}delete(t,n){const r=this;let s=!1;function a(i){if(i=ir(i),i){const o=v.findKey(r,i);o&&(!n||Xl(r,r[o],o,n))&&(delete r[o],s=!0)}}return v.isArray(t)?t.forEach(a):a(t),s}clear(t){const n=Object.keys(this);let r=n.length,s=!1;for(;r--;){const a=n[r];(!t||Xl(this,this[a],a,t,!0))&&(delete this[a],s=!0)}return s}normalize(t){const n=this,r={};return v.forEach(this,(s,a)=>{const i=v.findKey(r,a);if(i){n[i]=Es(s),delete n[a];return}const o=t?Mm(a):String(a).trim();o!==a&&delete n[a],n[o]=Es(s),r[o]=!0}),this}concat(...t){return this.constructor.concat(this,...t)}toJSON(t){const n=Object.create(null);return v.forEach(this,(r,s)=>{r!=null&&r!==!1&&(n[s]=t&&v.isArray(r)?r.join(", "):r)}),n}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([t,n])=>t+": "+n).join(` +`)}getSetCookie(){return this.get("set-cookie")||[]}get[Symbol.toStringTag](){return"AxiosHeaders"}static from(t){return t instanceof this?t:new this(t)}static concat(t,...n){const r=new this(t);return n.forEach(s=>r.set(s)),r}static accessor(t){const r=(this[Cu]=this[Cu]={accessors:{}}).accessors,s=this.prototype;function a(i){const o=ir(i);r[o]||(Dm(s,i),r[o]=!0)}return v.isArray(t)?t.forEach(a):a(t),this}};De.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]);v.reduceDescriptors(De.prototype,({value:e},t)=>{let n=t[0].toUpperCase()+t.slice(1);return{get:()=>e,set(r){this[n]=r}}});v.freezeMethods(De);function Yl(e,t){const n=this||Kr,r=t||n,s=De.from(r.headers);let a=r.data;return v.forEach(e,function(o){a=o.call(n,a,s.normalize(),t?t.status:void 0)}),s.normalize(),a}function xf(e){return!!(e&&e.__CANCEL__)}function Gn(e,t,n){$.call(this,e??"canceled",$.ERR_CANCELED,t,n),this.name="CanceledError"}v.inherits(Gn,$,{__CANCEL__:!0});function gf(e,t,n){const r=n.config.validateStatus;!n.status||!r||r(n.status)?e(n):t(new $("Request failed with status code "+n.status,[$.ERR_BAD_REQUEST,$.ERR_BAD_RESPONSE][Math.floor(n.status/100)-4],n.config,n.request,n))}function zm(e){const t=/^([-+\w]{1,25})(:?\/\/|:)/.exec(e);return t&&t[1]||""}function Am(e,t){e=e||10;const n=new Array(e),r=new Array(e);let s=0,a=0,i;return t=t!==void 0?t:1e3,function(u){const c=Date.now(),f=r[a];i||(i=c),n[s]=u,r[s]=c;let m=a,x=0;for(;m!==s;)x+=n[m++],m=m%e;if(s=(s+1)%e,s===a&&(a=(a+1)%e),c-i{n=f,s=null,a&&(clearTimeout(a),a=null),e(...c)};return[(...c)=>{const f=Date.now(),m=f-n;m>=r?i(c,f):(s=c,a||(a=setTimeout(()=>{a=null,i(s)},r-m)))},()=>s&&i(s)]}const Zs=(e,t,n=3)=>{let r=0;const s=Am(50,250);return Fm(a=>{const i=a.loaded,o=a.lengthComputable?a.total:void 0,u=i-r,c=s(u),f=i<=o;r=i;const m={loaded:i,total:o,progress:o?i/o:void 0,bytes:u,rate:c||void 0,estimated:c&&o&&f?(o-i)/c:void 0,event:a,lengthComputable:o!=null,[t?"download":"upload"]:!0};e(m)},n)},Eu=(e,t)=>{const n=e!=null;return[r=>t[0]({lengthComputable:n,total:e,loaded:r}),t[1]]},_u=e=>(...t)=>v.asap(()=>e(...t)),Im=we.hasStandardBrowserEnv?((e,t)=>n=>(n=new URL(n,we.origin),e.protocol===n.protocol&&e.host===n.host&&(t||e.port===n.port)))(new URL(we.origin),we.navigator&&/(msie|trident)/i.test(we.navigator.userAgent)):()=>!0,Um=we.hasStandardBrowserEnv?{write(e,t,n,r,s,a,i){if(typeof document>"u")return;const o=[`${e}=${encodeURIComponent(t)}`];v.isNumber(n)&&o.push(`expires=${new Date(n).toUTCString()}`),v.isString(r)&&o.push(`path=${r}`),v.isString(s)&&o.push(`domain=${s}`),a===!0&&o.push("secure"),v.isString(i)&&o.push(`SameSite=${i}`),document.cookie=o.join("; ")},read(e){if(typeof document>"u")return null;const t=document.cookie.match(new RegExp("(?:^|; )"+e+"=([^;]*)"));return t?decodeURIComponent(t[1]):null},remove(e){this.write(e,"",Date.now()-864e5,"/")}}:{write(){},read(){return null},remove(){}};function $m(e){return/^([a-z][a-z\d+\-.]*:)?\/\//i.test(e)}function Vm(e,t){return t?e.replace(/\/?\/$/,"")+"/"+t.replace(/^\/+/,""):e}function yf(e,t,n){let r=!$m(t);return e&&(r||n==!1)?Vm(e,t):t}const Ru=e=>e instanceof De?{...e}:e;function fn(e,t){t=t||{};const n={};function r(c,f,m,x){return v.isPlainObject(c)&&v.isPlainObject(f)?v.merge.call({caseless:x},c,f):v.isPlainObject(f)?v.merge({},f):v.isArray(f)?f.slice():f}function s(c,f,m,x){if(v.isUndefined(f)){if(!v.isUndefined(c))return r(void 0,c,m,x)}else return r(c,f,m,x)}function a(c,f){if(!v.isUndefined(f))return r(void 0,f)}function i(c,f){if(v.isUndefined(f)){if(!v.isUndefined(c))return r(void 0,c)}else return r(void 0,f)}function o(c,f,m){if(m in t)return r(c,f);if(m in e)return r(void 0,c)}const u={url:a,method:a,data:a,baseURL:i,transformRequest:i,transformResponse:i,paramsSerializer:i,timeout:i,timeoutMessage:i,withCredentials:i,withXSRFToken:i,adapter:i,responseType:i,xsrfCookieName:i,xsrfHeaderName:i,onUploadProgress:i,onDownloadProgress:i,decompress:i,maxContentLength:i,maxBodyLength:i,beforeRedirect:i,transport:i,httpAgent:i,httpsAgent:i,cancelToken:i,socketPath:i,responseEncoding:i,validateStatus:o,headers:(c,f,m)=>s(Ru(c),Ru(f),m,!0)};return v.forEach(Object.keys({...e,...t}),function(f){const m=u[f]||s,x=m(e[f],t[f],f);v.isUndefined(x)&&m!==o||(n[f]=x)}),n}const vf=e=>{const t=fn({},e);let{data:n,withXSRFToken:r,xsrfHeaderName:s,xsrfCookieName:a,headers:i,auth:o}=t;if(t.headers=i=De.from(i),t.url=pf(yf(t.baseURL,t.url,t.allowAbsoluteUrls),e.params,e.paramsSerializer),o&&i.set("Authorization","Basic "+btoa((o.username||"")+":"+(o.password?unescape(encodeURIComponent(o.password)):""))),v.isFormData(n)){if(we.hasStandardBrowserEnv||we.hasStandardBrowserWebWorkerEnv)i.setContentType(void 0);else if(v.isFunction(n.getHeaders)){const u=n.getHeaders(),c=["content-type","content-length"];Object.entries(u).forEach(([f,m])=>{c.includes(f.toLowerCase())&&i.set(f,m)})}}if(we.hasStandardBrowserEnv&&(r&&v.isFunction(r)&&(r=r(t)),r||r!==!1&&Im(t.url))){const u=s&&a&&Um.read(a);u&&i.set(s,u)}return t},Bm=typeof XMLHttpRequest<"u",Hm=Bm&&function(e){return new Promise(function(n,r){const s=vf(e);let a=s.data;const i=De.from(s.headers).normalize();let{responseType:o,onUploadProgress:u,onDownloadProgress:c}=s,f,m,x,y,g;function k(){y&&y(),g&&g(),s.cancelToken&&s.cancelToken.unsubscribe(f),s.signal&&s.signal.removeEventListener("abort",f)}let S=new XMLHttpRequest;S.open(s.method.toUpperCase(),s.url,!0),S.timeout=s.timeout;function p(){if(!S)return;const h=De.from("getAllResponseHeaders"in S&&S.getAllResponseHeaders()),C={data:!o||o==="text"||o==="json"?S.responseText:S.response,status:S.status,statusText:S.statusText,headers:h,config:e,request:S};gf(function(P){n(P),k()},function(P){r(P),k()},C),S=null}"onloadend"in S?S.onloadend=p:S.onreadystatechange=function(){!S||S.readyState!==4||S.status===0&&!(S.responseURL&&S.responseURL.indexOf("file:")===0)||setTimeout(p)},S.onabort=function(){S&&(r(new $("Request aborted",$.ECONNABORTED,e,S)),S=null)},S.onerror=function(N){const C=N&&N.message?N.message:"Network Error",L=new $(C,$.ERR_NETWORK,e,S);L.event=N||null,r(L),S=null},S.ontimeout=function(){let N=s.timeout?"timeout of "+s.timeout+"ms exceeded":"timeout exceeded";const C=s.transitional||hf;s.timeoutErrorMessage&&(N=s.timeoutErrorMessage),r(new $(N,C.clarifyTimeoutError?$.ETIMEDOUT:$.ECONNABORTED,e,S)),S=null},a===void 0&&i.setContentType(null),"setRequestHeader"in S&&v.forEach(i.toJSON(),function(N,C){S.setRequestHeader(C,N)}),v.isUndefined(s.withCredentials)||(S.withCredentials=!!s.withCredentials),o&&o!=="json"&&(S.responseType=s.responseType),c&&([x,g]=Zs(c,!0),S.addEventListener("progress",x)),u&&S.upload&&([m,y]=Zs(u),S.upload.addEventListener("progress",m),S.upload.addEventListener("loadend",y)),(s.cancelToken||s.signal)&&(f=h=>{S&&(r(!h||h.type?new Gn(null,e,S):h),S.abort(),S=null)},s.cancelToken&&s.cancelToken.subscribe(f),s.signal&&(s.signal.aborted?f():s.signal.addEventListener("abort",f)));const d=zm(s.url);if(d&&we.protocols.indexOf(d)===-1){r(new $("Unsupported protocol "+d+":",$.ERR_BAD_REQUEST,e));return}S.send(a||null)})},Wm=(e,t)=>{const{length:n}=e=e?e.filter(Boolean):[];if(t||n){let r=new AbortController,s;const a=function(c){if(!s){s=!0,o();const f=c instanceof Error?c:this.reason;r.abort(f instanceof $?f:new Gn(f instanceof Error?f.message:f))}};let i=t&&setTimeout(()=>{i=null,a(new $(`timeout ${t} of ms exceeded`,$.ETIMEDOUT))},t);const o=()=>{e&&(i&&clearTimeout(i),i=null,e.forEach(c=>{c.unsubscribe?c.unsubscribe(a):c.removeEventListener("abort",a)}),e=null)};e.forEach(c=>c.addEventListener("abort",a));const{signal:u}=r;return u.unsubscribe=()=>v.asap(o),u}},Qm=function*(e,t){let n=e.byteLength;if(n{const s=qm(e,t);let a=0,i,o=u=>{i||(i=!0,r&&r(u))};return new ReadableStream({async pull(u){try{const{done:c,value:f}=await s.next();if(c){o(),u.close();return}let m=f.byteLength;if(n){let x=a+=m;n(x)}u.enqueue(new Uint8Array(f))}catch(c){throw o(c),c}},cancel(u){return o(u),s.return()}},{highWaterMark:2})},Tu=64*1024,{isFunction:ps}=v,Jm=(({Request:e,Response:t})=>({Request:e,Response:t}))(v.global),{ReadableStream:Lu,TextEncoder:Ou}=v.global,Mu=(e,...t)=>{try{return!!e(...t)}catch{return!1}},Gm=e=>{e=v.merge.call({skipUndefined:!0},Jm,e);const{fetch:t,Request:n,Response:r}=e,s=t?ps(t):typeof fetch=="function",a=ps(n),i=ps(r);if(!s)return!1;const o=s&&ps(Lu),u=s&&(typeof Ou=="function"?(g=>k=>g.encode(k))(new Ou):async g=>new Uint8Array(await new n(g).arrayBuffer())),c=a&&o&&Mu(()=>{let g=!1;const k=new n(we.origin,{body:new Lu,method:"POST",get duplex(){return g=!0,"half"}}).headers.has("Content-Type");return g&&!k}),f=i&&o&&Mu(()=>v.isReadableStream(new r("").body)),m={stream:f&&(g=>g.body)};s&&["text","arrayBuffer","blob","formData","stream"].forEach(g=>{!m[g]&&(m[g]=(k,S)=>{let p=k&&k[g];if(p)return p.call(k);throw new $(`Response type '${g}' is not supported`,$.ERR_NOT_SUPPORT,S)})});const x=async g=>{if(g==null)return 0;if(v.isBlob(g))return g.size;if(v.isSpecCompliantForm(g))return(await new n(we.origin,{method:"POST",body:g}).arrayBuffer()).byteLength;if(v.isArrayBufferView(g)||v.isArrayBuffer(g))return g.byteLength;if(v.isURLSearchParams(g)&&(g=g+""),v.isString(g))return(await u(g)).byteLength},y=async(g,k)=>{const S=v.toFiniteNumber(g.getContentLength());return S??x(k)};return async g=>{let{url:k,method:S,data:p,signal:d,cancelToken:h,timeout:N,onDownloadProgress:C,onUploadProgress:L,responseType:P,headers:b,withCredentials:U="same-origin",fetchOptions:w}=vf(g),z=t||fetch;P=P?(P+"").toLowerCase():"text";let W=Wm([d,h&&h.toAbortSignal()],N),G=null;const Y=W&&W.unsubscribe&&(()=>{W.unsubscribe()});let j;try{if(L&&c&&S!=="get"&&S!=="head"&&(j=await y(b,p))!==0){let Q=new n(k,{method:"POST",body:p,duplex:"half"}),J;if(v.isFormData(p)&&(J=Q.headers.get("content-type"))&&b.setContentType(J),Q.body){const[Ge,Ee]=Eu(j,Zs(_u(L)));p=Pu(Q.body,Tu,Ge,Ee)}}v.isString(U)||(U=U?"include":"omit");const I=a&&"credentials"in n.prototype,V={...w,signal:W,method:S.toUpperCase(),headers:b.normalize().toJSON(),body:p,duplex:"half",credentials:I?U:void 0};G=a&&new n(k,V);let _=await(a?z(G,w):z(k,V));const M=f&&(P==="stream"||P==="response");if(f&&(C||M&&Y)){const Q={};["status","statusText","headers"].forEach(Ct=>{Q[Ct]=_[Ct]});const J=v.toFiniteNumber(_.headers.get("content-length")),[Ge,Ee]=C&&Eu(J,Zs(_u(C),!0))||[];_=new r(Pu(_.body,Tu,Ge,()=>{Ee&&Ee(),Y&&Y()}),Q)}P=P||"text";let A=await m[v.findKey(m,P)||"text"](_,g);return!M&&Y&&Y(),await new Promise((Q,J)=>{gf(Q,J,{data:A,headers:De.from(_.headers),status:_.status,statusText:_.statusText,config:g,request:G})})}catch(I){throw Y&&Y(),I&&I.name==="TypeError"&&/Load failed|fetch/i.test(I.message)?Object.assign(new $("Network Error",$.ERR_NETWORK,g,G),{cause:I.cause||I}):$.from(I,I&&I.code,g,G)}}},Xm=new Map,wf=e=>{let t=e&&e.env||{};const{fetch:n,Request:r,Response:s}=t,a=[r,s,n];let i=a.length,o=i,u,c,f=Xm;for(;o--;)u=a[o],c=f.get(u),c===void 0&&f.set(u,c=o?new Map:Gm(t)),f=c;return c};wf();const no={http:hm,xhr:Hm,fetch:{get:wf}};v.forEach(no,(e,t)=>{if(e){try{Object.defineProperty(e,"name",{value:t})}catch{}Object.defineProperty(e,"adapterName",{value:t})}});const Du=e=>`- ${e}`,Ym=e=>v.isFunction(e)||e===null||e===!1;function Zm(e,t){e=v.isArray(e)?e:[e];const{length:n}=e;let r,s;const a={};for(let i=0;i`adapter ${u} `+(c===!1?"is not supported by the environment":"is not available in the build"));let o=n?i.length>1?`since : +`+i.map(Du).join(` +`):" "+Du(i[0]):"as no adapter specified";throw new $("There is no suitable adapter to dispatch the request "+o,"ERR_NOT_SUPPORT")}return s}const kf={getAdapter:Zm,adapters:no};function Zl(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new Gn(null,e)}function zu(e){return Zl(e),e.headers=De.from(e.headers),e.data=Yl.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),kf.getAdapter(e.adapter||Kr.adapter,e)(e).then(function(r){return Zl(e),r.data=Yl.call(e,e.transformResponse,r),r.headers=De.from(r.headers),r},function(r){return xf(r)||(Zl(e),r&&r.response&&(r.response.data=Yl.call(e,e.transformResponse,r.response),r.response.headers=De.from(r.response.headers))),Promise.reject(r)})}const bf="1.13.2",kl={};["object","boolean","number","function","string","symbol"].forEach((e,t)=>{kl[e]=function(r){return typeof r===e||"a"+(t<1?"n ":" ")+e}});const Au={};kl.transitional=function(t,n,r){function s(a,i){return"[Axios v"+bf+"] Transitional option '"+a+"'"+i+(r?". "+r:"")}return(a,i,o)=>{if(t===!1)throw new $(s(i," has been removed"+(n?" in "+n:"")),$.ERR_DEPRECATED);return n&&!Au[i]&&(Au[i]=!0,console.warn(s(i," has been deprecated since v"+n+" and will be removed in the near future"))),t?t(a,i,o):!0}};kl.spelling=function(t){return(n,r)=>(console.warn(`${r} is likely a misspelling of ${t}`),!0)};function ex(e,t,n){if(typeof e!="object")throw new $("options must be an object",$.ERR_BAD_OPTION_VALUE);const r=Object.keys(e);let s=r.length;for(;s-- >0;){const a=r[s],i=t[a];if(i){const o=e[a],u=o===void 0||i(o,a,e);if(u!==!0)throw new $("option "+a+" must be "+u,$.ERR_BAD_OPTION_VALUE);continue}if(n!==!0)throw new $("Unknown option "+a,$.ERR_BAD_OPTION)}}const _s={assertOptions:ex,validators:kl},ut=_s.validators;let ln=class{constructor(t){this.defaults=t||{},this.interceptors={request:new Su,response:new Su}}async request(t,n){try{return await this._request(t,n)}catch(r){if(r instanceof Error){let s={};Error.captureStackTrace?Error.captureStackTrace(s):s=new Error;const a=s.stack?s.stack.replace(/^.+\n/,""):"";try{r.stack?a&&!String(r.stack).endsWith(a.replace(/^.+\n.+\n/,""))&&(r.stack+=` +`+a):r.stack=a}catch{}}throw r}}_request(t,n){typeof t=="string"?(n=n||{},n.url=t):n=t||{},n=fn(this.defaults,n);const{transitional:r,paramsSerializer:s,headers:a}=n;r!==void 0&&_s.assertOptions(r,{silentJSONParsing:ut.transitional(ut.boolean),forcedJSONParsing:ut.transitional(ut.boolean),clarifyTimeoutError:ut.transitional(ut.boolean)},!1),s!=null&&(v.isFunction(s)?n.paramsSerializer={serialize:s}:_s.assertOptions(s,{encode:ut.function,serialize:ut.function},!0)),n.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls!==void 0?n.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls:n.allowAbsoluteUrls=!0),_s.assertOptions(n,{baseUrl:ut.spelling("baseURL"),withXsrfToken:ut.spelling("withXSRFToken")},!0),n.method=(n.method||this.defaults.method||"get").toLowerCase();let i=a&&v.merge(a.common,a[n.method]);a&&v.forEach(["delete","get","head","post","put","patch","common"],g=>{delete a[g]}),n.headers=De.concat(i,a);const o=[];let u=!0;this.interceptors.request.forEach(function(k){typeof k.runWhen=="function"&&k.runWhen(n)===!1||(u=u&&k.synchronous,o.unshift(k.fulfilled,k.rejected))});const c=[];this.interceptors.response.forEach(function(k){c.push(k.fulfilled,k.rejected)});let f,m=0,x;if(!u){const g=[zu.bind(this),void 0];for(g.unshift(...o),g.push(...c),x=g.length,f=Promise.resolve(n);m{if(!r._listeners)return;let a=r._listeners.length;for(;a-- >0;)r._listeners[a](s);r._listeners=null}),this.promise.then=s=>{let a;const i=new Promise(o=>{r.subscribe(o),a=o}).then(s);return i.cancel=function(){r.unsubscribe(a)},i},t(function(a,i,o){r.reason||(r.reason=new Gn(a,i,o),n(r.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(t){if(this.reason){t(this.reason);return}this._listeners?this._listeners.push(t):this._listeners=[t]}unsubscribe(t){if(!this._listeners)return;const n=this._listeners.indexOf(t);n!==-1&&this._listeners.splice(n,1)}toAbortSignal(){const t=new AbortController,n=r=>{t.abort(r)};return this.subscribe(n),t.signal.unsubscribe=()=>this.unsubscribe(n),t.signal}static source(){let t;return{token:new Nf(function(s){t=s}),cancel:t}}};function nx(e){return function(n){return e.apply(null,n)}}function rx(e){return v.isObject(e)&&e.isAxiosError===!0}const Za={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511,WebServerIsDown:521,ConnectionTimedOut:522,OriginIsUnreachable:523,TimeoutOccurred:524,SslHandshakeFailed:525,InvalidSslCertificate:526};Object.entries(Za).forEach(([e,t])=>{Za[t]=e});function jf(e){const t=new ln(e),n=ef(ln.prototype.request,t);return v.extend(n,ln.prototype,t,{allOwnKeys:!0}),v.extend(n,t,null,{allOwnKeys:!0}),n.create=function(s){return jf(fn(e,s))},n}const D=jf(Kr);D.Axios=ln;D.CanceledError=Gn;D.CancelToken=tx;D.isCancel=xf;D.VERSION=bf;D.toFormData=wl;D.AxiosError=$;D.Cancel=D.CanceledError;D.all=function(t){return Promise.all(t)};D.spread=nx;D.isAxiosError=rx;D.mergeConfig=fn;D.AxiosHeaders=De;D.formToJSON=e=>mf(v.isHTMLForm(e)?new FormData(e):e);D.getAdapter=kf.getAdapter;D.HttpStatusCode=Za;D.default=D;const{Axios:Dx,AxiosError:zx,CanceledError:Ax,isCancel:Fx,CancelToken:Ix,VERSION:Ux,all:$x,Cancel:Vx,isAxiosError:Bx,spread:Hx,toFormData:Wx,AxiosHeaders:Qx,HttpStatusCode:qx,formToJSON:Kx,getAdapter:Jx,mergeConfig:Gx}=D;/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */var sx={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round"};/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const lx=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase().trim(),F=(e,t)=>{const n=E.forwardRef(({color:r="currentColor",size:s=24,strokeWidth:a=2,absoluteStrokeWidth:i,className:o="",children:u,...c},f)=>E.createElement("svg",{ref:f,...sx,width:s,height:s,stroke:r,strokeWidth:i?Number(a)*24/Number(s):a,className:["lucide",`lucide-${lx(e)}`,o].join(" "),...c},[...t.map(([m,x])=>E.createElement(m,x)),...Array.isArray(u)?u:[u]]));return n.displayName=`${e}`,n};/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ax=F("Activity",[["path",{d:"M22 12h-4l-3 9L9 3l-3 9H2",key:"d5dnw9"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ix=F("AlertTriangle",[["path",{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z",key:"c3ski4"}],["path",{d:"M12 9v4",key:"juzpu7"}],["path",{d:"M12 17h.01",key:"p32p05"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Sf=F("ArrowDownUp",[["path",{d:"m3 16 4 4 4-4",key:"1co6wj"}],["path",{d:"M7 20V4",key:"1yoxec"}],["path",{d:"m21 8-4-4-4 4",key:"1c9v7m"}],["path",{d:"M17 4v16",key:"7dpous"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ox=F("Ban",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m4.9 4.9 14.2 14.2",key:"1m5liu"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Pt=F("Bot",[["path",{d:"M12 8V4H8",key:"hb8ula"}],["rect",{width:"16",height:"12",x:"4",y:"8",rx:"2",key:"enze0r"}],["path",{d:"M2 14h2",key:"vft8re"}],["path",{d:"M20 14h2",key:"4cs60a"}],["path",{d:"M15 13v2",key:"1xurst"}],["path",{d:"M9 13v2",key:"rq6x2g"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ea=F("Briefcase",[["rect",{width:"20",height:"14",x:"2",y:"7",rx:"2",ry:"2",key:"eto64e"}],["path",{d:"M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16",key:"zwj3tp"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ir=F("Building",[["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2",ry:"2",key:"76otgf"}],["path",{d:"M9 22v-4h6v4",key:"r93iot"}],["path",{d:"M8 6h.01",key:"1dz90k"}],["path",{d:"M16 6h.01",key:"1x0f13"}],["path",{d:"M12 6h.01",key:"1vi96p"}],["path",{d:"M12 10h.01",key:"1nrarc"}],["path",{d:"M12 14h.01",key:"1etili"}],["path",{d:"M16 10h.01",key:"1m94wz"}],["path",{d:"M16 14h.01",key:"1gbofw"}],["path",{d:"M8 10h.01",key:"19clt8"}],["path",{d:"M8 14h.01",key:"6423bh"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ux=F("Calculator",[["rect",{width:"16",height:"20",x:"4",y:"2",rx:"2",key:"1nb95v"}],["line",{x1:"8",x2:"16",y1:"6",y2:"6",key:"x4nwl0"}],["line",{x1:"16",x2:"16",y1:"14",y2:"18",key:"wjye3r"}],["path",{d:"M16 10h.01",key:"1m94wz"}],["path",{d:"M12 10h.01",key:"1nrarc"}],["path",{d:"M8 10h.01",key:"19clt8"}],["path",{d:"M12 14h.01",key:"1etili"}],["path",{d:"M8 14h.01",key:"6423bh"}],["path",{d:"M12 18h.01",key:"mhygvu"}],["path",{d:"M8 18h.01",key:"lrp35t"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Pn=F("Check",[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const cx=F("ChevronDown",[["path",{d:"m6 9 6 6 6-6",key:"qrunsl"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Cf=F("ChevronLeft",[["path",{d:"m15 18-6-6 6-6",key:"1wnfg3"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Ef=F("ChevronRight",[["path",{d:"m9 18 6-6-6-6",key:"mthhwq"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ta=F("Clock",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["polyline",{points:"12 6 12 12 16 14",key:"68esgv"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const dx=F("Database",[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3",key:"msslwz"}],["path",{d:"M3 5V19A9 3 0 0 0 21 19V5",key:"1wlel7"}],["path",{d:"M3 12A9 3 0 0 0 21 12",key:"mv7ke4"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const fx=F("DollarSign",[["line",{x1:"12",x2:"12",y1:"2",y2:"22",key:"7eqyqh"}],["path",{d:"M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6",key:"1b0p4s"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const _f=F("Download",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"7 10 12 15 17 10",key:"2ggqvy"}],["line",{x1:"12",x2:"12",y1:"15",y2:"3",key:"1vk2je"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ei=F("ExternalLink",[["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}],["polyline",{points:"15 3 21 3 21 9",key:"mznyad"}],["line",{x1:"10",x2:"21",y1:"14",y2:"3",key:"18c3s4"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const px=F("Filter",[["polygon",{points:"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3",key:"1yg77f"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const el=F("Flag",[["path",{d:"M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z",key:"i9b6wo"}],["line",{x1:"4",x2:"4",y1:"22",y2:"15",key:"1cm3nv"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const fr=F("Globe",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20",key:"13o1zl"}],["path",{d:"M2 12h20",key:"9i4pu4"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const hx=F("Grid3x3",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}],["path",{d:"M3 9h18",key:"1pudct"}],["path",{d:"M3 15h18",key:"5xshup"}],["path",{d:"M9 3v18",key:"fh3hqa"}],["path",{d:"M15 3v18",key:"14nvp0"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const mx=F("LayoutDashboard",[["rect",{width:"7",height:"9",x:"3",y:"3",rx:"1",key:"10lvy0"}],["rect",{width:"7",height:"5",x:"14",y:"3",rx:"1",key:"16une8"}],["rect",{width:"7",height:"9",x:"14",y:"12",rx:"1",key:"1hutg5"}],["rect",{width:"7",height:"5",x:"3",y:"16",rx:"1",key:"ldoo1y"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Rf=F("LayoutGrid",[["rect",{width:"7",height:"7",x:"3",y:"3",rx:"1",key:"1g98yp"}],["rect",{width:"7",height:"7",x:"14",y:"3",rx:"1",key:"6d4xhi"}],["rect",{width:"7",height:"7",x:"14",y:"14",rx:"1",key:"nxv5o0"}],["rect",{width:"7",height:"7",x:"3",y:"14",rx:"1",key:"1bb6yr"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Pf=F("List",[["line",{x1:"8",x2:"21",y1:"6",y2:"6",key:"7ey8pc"}],["line",{x1:"8",x2:"21",y1:"12",y2:"12",key:"rjfblc"}],["line",{x1:"8",x2:"21",y1:"18",y2:"18",key:"c3b1m8"}],["line",{x1:"3",x2:"3.01",y1:"6",y2:"6",key:"1g7gq3"}],["line",{x1:"3",x2:"3.01",y1:"12",y2:"12",key:"1pjlvk"}],["line",{x1:"3",x2:"3.01",y1:"18",y2:"18",key:"28t2mc"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Fu=F("Loader2",[["path",{d:"M21 12a9 9 0 1 1-6.219-8.56",key:"13zald"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Iu=F("Lock",[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2",key:"1w4ew1"}],["path",{d:"M7 11V7a5 5 0 0 1 10 0v4",key:"fwvmzm"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Tf=F("Mail",[["rect",{width:"20",height:"16",x:"2",y:"4",rx:"2",key:"18n3k1"}],["path",{d:"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7",key:"1ocrg3"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ti=F("MapPin",[["path",{d:"M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z",key:"2oe9fu"}],["circle",{cx:"12",cy:"10",r:"3",key:"ilqhr7"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const xx=F("Moon",[["path",{d:"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z",key:"a7tn18"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const gx=F("Pen",[["path",{d:"M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z",key:"5qss01"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const na=F("Pencil",[["path",{d:"M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z",key:"5qss01"}],["path",{d:"m15 5 4 4",key:"1mk7zo"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Uu=F("Play",[["polygon",{points:"5 3 19 12 5 21 5 3",key:"191637"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ni=F("Plus",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"M12 5v14",key:"s699le"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const ri=F("RefreshCw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const yx=F("Ruler",[["path",{d:"M21.3 15.3a2.4 2.4 0 0 1 0 3.4l-2.6 2.6a2.4 2.4 0 0 1-3.4 0L2.7 8.7a2.41 2.41 0 0 1 0-3.4l2.6-2.6a2.41 2.41 0 0 1 3.4 0Z",key:"icamh8"}],["path",{d:"m14.5 12.5 2-2",key:"inckbg"}],["path",{d:"m11.5 9.5 2-2",key:"fmmyf7"}],["path",{d:"m8.5 6.5 2-2",key:"vc6u1g"}],["path",{d:"m17.5 15.5 2-2",key:"wo5hmg"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Lf=F("Save",[["path",{d:"M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z",key:"1owoqh"}],["polyline",{points:"17 21 17 13 7 13 7 21",key:"1md35c"}],["polyline",{points:"7 3 7 8 15 8",key:"8nz8an"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const vx=F("Scale",[["path",{d:"m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z",key:"7g6ntu"}],["path",{d:"m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z",key:"ijws7r"}],["path",{d:"M7 21h10",key:"1b0cd5"}],["path",{d:"M12 3v18",key:"108xh3"}],["path",{d:"M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2",key:"3gwbw2"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const An=F("Search",[["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}],["path",{d:"m21 21-4.3-4.3",key:"1qie3q"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const wx=F("Settings",[["path",{d:"M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z",key:"1qme2f"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const kx=F("Star",[["polygon",{points:"12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2",key:"8f66p6"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const bx=F("Sun",[["circle",{cx:"12",cy:"12",r:"4",key:"4exip2"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"m4.93 4.93 1.41 1.41",key:"149t6j"}],["path",{d:"m17.66 17.66 1.41 1.41",key:"ptbguv"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"m6.34 17.66-1.41 1.41",key:"1m8zz5"}],["path",{d:"m19.07 4.93-1.41 1.41",key:"1shlcs"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const si=F("Tag",[["path",{d:"M12 2H2v10l9.29 9.29c.94.94 2.48.94 3.42 0l6.58-6.58c.94-.94.94-2.48 0-3.42L12 2Z",key:"14b2ls"}],["path",{d:"M7 7h.01",key:"7u93v4"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Of=F("Target",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["circle",{cx:"12",cy:"12",r:"6",key:"1vlfrh"}],["circle",{cx:"12",cy:"12",r:"2",key:"1c9p78"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Mf=F("Trash2",[["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6",key:"4alrt4"}],["path",{d:"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2",key:"v07s0e"}],["line",{x1:"10",x2:"10",y1:"11",y2:"17",key:"1uufr5"}],["line",{x1:"14",x2:"14",y1:"11",y2:"17",key:"xtxkd"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const $u=F("Unlock",[["rect",{width:"18",height:"11",x:"3",y:"11",rx:"2",ry:"2",key:"1w4ew1"}],["path",{d:"M7 11V7a5 5 0 0 1 9.9-1",key:"1mm8w8"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Df=F("UploadCloud",[["path",{d:"M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242",key:"1pljnt"}],["path",{d:"M12 12v9",key:"192myk"}],["path",{d:"m16 16-4-4-4 4",key:"119tzi"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const zf=F("Upload",[["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["polyline",{points:"17 8 12 3 7 8",key:"t8dd8p"}],["line",{x1:"12",x2:"12",y1:"3",y2:"15",key:"widbto"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Nx=F("User",[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",key:"975kel"}],["circle",{cx:"12",cy:"7",r:"4",key:"17ys0d"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const Wt=F("Users",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}],["path",{d:"M22 21v-2a4 4 0 0 0-3-3.87",key:"kshegd"}],["path",{d:"M16 3.13a4 4 0 0 1 0 7.75",key:"1da9ce"}]]);/** + * @license lucide-react v0.294.0 - ISC + * + * This source code is licensed under the ISC license. + * See the LICENSE file in the root directory of this source tree. + */const rt=F("X",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]]);function Af(e){var t,n,r="";if(typeof e=="string"||typeof e=="number")r+=e;else if(typeof e=="object")if(Array.isArray(e)){var s=e.length;for(t=0;t{k(!0);try{const b=await D.get(`${e}/companies?skip=${u*N}&limit=${N}&search=${f}&sort_by=${x}`);a(b.data.items),o(b.data.total)}catch(b){console.error("Failed to fetch companies",b)}finally{k(!1)}};E.useEffect(()=>{const b=setTimeout(C,300);return()=>clearTimeout(b)},[u,f,n,x]);const L=async b=>{p(b);try{await D.post(`${e}/enrich/discover`,{company_id:b}),setTimeout(C,2e3)}catch{alert("Discovery Error")}finally{p(null)}},P=async b=>{p(b);try{await D.post(`${e}/enrich/analyze`,{company_id:b}),setTimeout(C,2e3)}catch{alert("Analysis Error")}finally{p(null)}};return l.jsxs("div",{className:"flex flex-col h-full bg-white dark:bg-slate-900 transition-colors",children:[l.jsxs("div",{className:"flex flex-col md:flex-row gap-4 p-4 border-b border-slate-200 dark:border-slate-800 items-center justify-between bg-slate-50 dark:bg-slate-950/50",children:[l.jsxs("div",{className:"flex items-center gap-2 text-slate-700 dark:text-slate-300 font-bold text-lg",children:[l.jsx(Ir,{className:"h-5 w-5"}),l.jsxs("h2",{children:["Companies (",i,")"]})]}),l.jsxs("div",{className:"flex flex-1 w-full md:w-auto items-center gap-2 max-w-2xl",children:[l.jsxs("div",{className:"relative flex-1",children:[l.jsx(An,{className:"absolute left-3 top-2.5 h-4 w-4 text-slate-400"}),l.jsx("input",{type:"text",placeholder:"Search companies...",className:"w-full pl-10 pr-4 py-2 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-700 rounded-md text-sm text-slate-900 dark:text-slate-200 focus:ring-2 focus:ring-blue-500 outline-none",value:f,onChange:b=>{m(b.target.value),c(0)}})]}),l.jsxs("div",{className:"relative flex items-center text-slate-700 dark:text-slate-300",children:[l.jsx(Sf,{className:"absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400 pointer-events-none"}),l.jsxs("select",{value:x,onChange:b=>y(b.target.value),className:"pl-8 pr-4 py-2 appearance-none bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-700 rounded-md text-sm focus:ring-2 focus:ring-blue-500 outline-none",children:[l.jsx("option",{value:"name_asc",children:"Alphabetical"}),l.jsx("option",{value:"created_desc",children:"Newest First"}),l.jsx("option",{value:"updated_desc",children:"Last Modified"})]})]}),l.jsxs("div",{className:"flex items-center bg-slate-200 dark:bg-slate-800 p-1 rounded-md text-slate-700 dark:text-slate-300",children:[l.jsx("button",{onClick:()=>h("grid"),className:B("p-1.5 rounded",d==="grid"&&"bg-white dark:bg-slate-700 shadow text-blue-600 dark:text-white"),title:"Grid View",children:l.jsx(Rf,{className:"h-4 w-4"})}),l.jsx("button",{onClick:()=>h("list"),className:B("p-1.5 rounded",d==="list"&&"bg-white dark:bg-slate-700 shadow text-blue-600 dark:text-white"),title:"List View",children:l.jsx(Pf,{className:"h-4 w-4"})})]}),l.jsxs("button",{onClick:r,className:"flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white text-sm font-bold rounded-md shadow-sm",children:[l.jsx(zf,{className:"h-4 w-4"})," ",l.jsx("span",{className:"hidden md:inline",children:"Import"})]})]})]}),l.jsxs("div",{className:"flex-1 overflow-auto bg-slate-50 dark:bg-slate-950/30",children:[g&&l.jsx("div",{className:"p-4 text-center text-slate-500",children:"Loading companies..."}),s.length===0&&!g?l.jsxs("div",{className:"p-12 text-center text-slate-500",children:[l.jsx(Ir,{className:"h-12 w-12 mx-auto mb-4 opacity-20"}),l.jsx("p",{className:"text-lg font-medium",children:"No companies found"}),l.jsx("p",{className:"text-slate-400 mt-2",children:"Import a list or create one manually to get started."})]}):d==="grid"?l.jsx("div",{className:"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 p-4",children:s.map(b=>l.jsxs("div",{onClick:()=>t(b.id),className:"bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-lg p-4 hover:shadow-lg transition-all flex flex-col gap-3 group cursor-pointer border-l-4",style:{borderLeftColor:b.status==="ENRICHED"?"#22c55e":b.status==="DISCOVERED"?"#3b82f6":"#94a3b8"},children:[l.jsxs("div",{className:"flex items-start justify-between",children:[l.jsxs("div",{className:"min-w-0 flex-1",children:[l.jsxs("div",{className:"flex items-center gap-2",children:[l.jsx(el,{className:B("h-3 w-3 text-slate-300 dark:text-slate-600",b.has_pending_mistakes&&"text-red-500 fill-red-500")}),l.jsx("div",{className:"font-bold text-slate-900 dark:text-white text-sm truncate",title:b.name,children:b.name})]}),l.jsx("div",{className:"flex items-center gap-1 text-[10px] text-slate-500 dark:text-slate-400 font-medium",children:b.city&&b.country?l.jsxs(l.Fragment,{children:[l.jsx(ti,{className:"h-3 w-3"})," ",b.city," ",l.jsxs("span",{className:"text-slate-400",children:["(",b.country,")"]})]}):l.jsx("span",{className:"italic opacity-50",children:"-"})})]}),l.jsx("div",{className:"flex gap-1 ml-2",children:S===b.id?l.jsx(Fu,{className:"h-4 w-4 animate-spin text-blue-500"}):b.status==="NEW"||!b.website||b.website==="k.A."?l.jsx("button",{onClick:U=>{U.stopPropagation(),L(b.id)},className:"p-1.5 bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 rounded hover:bg-blue-600 hover:text-white transition-colors",children:l.jsx(An,{className:"h-3.5 w-3.5"})}):l.jsx("button",{onClick:U=>{U.stopPropagation(),P(b.id)},className:"p-1.5 bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 rounded hover:bg-blue-600 hover:text-white transition-colors",children:l.jsx(Uu,{className:"h-3.5 w-3.5 fill-current"})})})]}),l.jsxs("div",{className:"space-y-2 pt-2 border-t border-slate-100 dark:border-slate-800/50",children:[b.website&&b.website!=="k.A."?l.jsxs("div",{className:"flex items-center gap-2 text-xs text-blue-600 dark:text-blue-400 font-medium truncate",children:[l.jsx(fr,{className:"h-3 w-3"}),l.jsx("span",{children:new URL(b.website).hostname.replace("www.","")})]}):l.jsx("div",{className:"text-xs text-slate-400 italic",children:"No website found"}),l.jsx("div",{className:"text-[10px] text-slate-500 uppercase font-bold tracking-wider truncate",children:b.industry_ai||"Industry Pending"})]})]},b.id))}):l.jsxs("table",{className:"min-w-full divide-y divide-slate-200 dark:divide-slate-800",children:[l.jsx("thead",{className:"bg-slate-100 dark:bg-slate-950/50",children:l.jsxs("tr",{children:[l.jsx("th",{scope:"col",className:"px-3 py-3.5 text-left text-sm font-semibold text-slate-900 dark:text-white",children:"Company"}),l.jsx("th",{scope:"col",className:"px-3 py-3.5 text-left text-sm font-semibold text-slate-900 dark:text-white",children:"Location"}),l.jsx("th",{scope:"col",className:"px-3 py-3.5 text-left text-sm font-semibold text-slate-900 dark:text-white",children:"Website"}),l.jsx("th",{scope:"col",className:"px-3 py-3.5 text-left text-sm font-semibold text-slate-900 dark:text-white",children:"AI Industry"}),l.jsx("th",{scope:"col",className:"relative px-3 py-3.5",children:l.jsx("span",{className:"sr-only",children:"Actions"})})]})}),l.jsx("tbody",{className:"divide-y divide-slate-200 dark:divide-slate-800 bg-white dark:bg-slate-900",children:s.map(b=>l.jsxs("tr",{onClick:()=>t(b.id),className:"hover:bg-slate-50 dark:hover:bg-slate-800/50 cursor-pointer",children:[l.jsx("td",{className:"whitespace-nowrap px-3 py-4 text-sm font-medium text-slate-900 dark:text-white",children:l.jsxs("div",{className:"flex items-center gap-2",children:[l.jsx(el,{className:B("h-3 w-3 text-slate-300 dark:text-slate-600",b.has_pending_mistakes&&"text-red-500 fill-red-500")}),l.jsx("span",{children:b.name})]})}),l.jsx("td",{className:"whitespace-nowrap px-3 py-4 text-sm text-slate-500 dark:text-slate-400",children:b.city&&b.country?`${b.city}, (${b.country})`:"-"}),l.jsx("td",{className:"whitespace-nowrap px-3 py-4 text-sm text-blue-600 dark:text-blue-400",children:b.website&&b.website!=="k.A."?l.jsx("a",{href:b.website,target:"_blank",rel:"noreferrer",children:new URL(b.website).hostname.replace("www.","")}):"n/a"}),l.jsx("td",{className:"whitespace-nowrap px-3 py-4 text-sm text-slate-500 dark:text-slate-400",children:b.industry_ai||"Pending"}),l.jsx("td",{className:"relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0",children:S===b.id?l.jsx(Fu,{className:"h-4 w-4 animate-spin text-blue-500"}):b.status==="NEW"||!b.website||b.website==="k.A."?l.jsx("button",{onClick:U=>{U.stopPropagation(),L(b.id)},className:"text-slate-600 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400",children:l.jsx(An,{className:"h-4 w-4"})}):l.jsx("button",{onClick:U=>{U.stopPropagation(),P(b.id)},className:"text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300",children:l.jsx(Uu,{className:"h-4 w-4 fill-current"})})})]},b.id))})]})]}),l.jsxs("div",{className:"p-3 border-t border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 flex justify-between items-center text-xs text-slate-500 dark:text-slate-400",children:[l.jsxs("span",{children:[i," Companies total"]}),l.jsxs("div",{className:"flex gap-1 items-center",children:[l.jsx("button",{disabled:u===0,onClick:()=>c(b=>b-1),className:"p-1 rounded hover:bg-slate-100 dark:hover:bg-slate-800 disabled:opacity-30",children:l.jsx(Cf,{className:"h-4 w-4"})}),l.jsxs("span",{children:["Page ",u+1]}),l.jsx("button",{disabled:(u+1)*N>=i,onClick:()=>c(b=>b+1),className:"p-1 rounded hover:bg-slate-100 dark:hover:bg-slate-800 disabled:opacity-30",children:l.jsx(Ef,{className:"h-4 w-4"})})]})]})]})}function Sx({apiBase:e,onCompanyClick:t,onContactClick:n}){const[r,s]=E.useState([]),[a,i]=E.useState(0),[o,u]=E.useState(0),[c,f]=E.useState(""),[m,x]=E.useState("name_asc"),[y,g]=E.useState(!1),[k,S]=E.useState("grid"),p=50,[d,h]=E.useState(!1),[N,C]=E.useState(""),[L,P]=E.useState(null),b=()=>{g(!0),D.get(`${e}/contacts/all?skip=${o*p}&limit=${p}&search=${c}&sort_by=${m}`).then(w=>{s(w.data.items),i(w.data.total)}).finally(()=>g(!1))};E.useEffect(()=>{const w=setTimeout(b,300);return()=>clearTimeout(w)},[o,c,m]);const U=async()=>{if(N){P("Parsing...");try{const z=N.split(` +`).filter(G=>G.trim()).map(G=>{const Y=G.split(/[;,|]+/).map(j=>j.trim());return Y.length<3?null:{company_name:Y[0],first_name:Y[1],last_name:Y[2],email:Y[3]||null,job_title:Y[4]||null}}).filter(Boolean);if(z.length===0){P("Error: No valid contacts found. Format: Company, First, Last, Email");return}P(`Importing ${z.length} contacts...`);const W=await D.post(`${e}/contacts/bulk`,{contacts:z});P(`Success! Added: ${W.data.added}, Created Companies: ${W.data.companies_created}, Skipped: ${W.data.skipped}`),C(""),setTimeout(()=>{h(!1),P(null),b()},2e3)}catch(w){console.error(w),P("Import Failed.")}}};return l.jsxs("div",{className:"flex flex-col h-full bg-white dark:bg-slate-900 transition-colors",children:[l.jsxs("div",{className:"flex flex-col md:flex-row gap-4 p-4 border-b border-slate-200 dark:border-slate-800 items-center justify-between bg-slate-50 dark:bg-slate-950/50",children:[l.jsxs("div",{className:"flex items-center gap-2 text-slate-700 dark:text-slate-300 font-bold text-lg",children:[l.jsx(Wt,{className:"h-5 w-5"}),l.jsxs("h2",{children:["All Contacts (",a,")"]})]}),l.jsxs("div",{className:"flex flex-1 w-full md:w-auto items-center gap-2 max-w-2xl",children:[l.jsxs("div",{className:"relative flex-1",children:[l.jsx(An,{className:"absolute left-3 top-2.5 h-4 w-4 text-slate-400"}),l.jsx("input",{type:"text",placeholder:"Search contacts...",className:"w-full pl-10 pr-4 py-2 bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-700 rounded-md text-sm text-slate-900 dark:text-slate-200 focus:ring-2 focus:ring-blue-500 outline-none",value:c,onChange:w=>{f(w.target.value),u(0)}})]}),l.jsxs("div",{className:"relative flex items-center text-slate-700 dark:text-slate-300",children:[l.jsx(Sf,{className:"absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-slate-400 pointer-events-none"}),l.jsxs("select",{value:m,onChange:w=>x(w.target.value),className:"pl-8 pr-4 py-2 appearance-none bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-700 rounded-md text-sm focus:ring-2 focus:ring-blue-500 outline-none",children:[l.jsx("option",{value:"name_asc",children:"Alphabetical"}),l.jsx("option",{value:"created_desc",children:"Newest First"}),l.jsx("option",{value:"updated_desc",children:"Last Modified"})]})]}),l.jsxs("div",{className:"flex items-center bg-slate-200 dark:bg-slate-800 p-1 rounded-md text-slate-700 dark:text-slate-300",children:[l.jsx("button",{onClick:()=>S("grid"),className:B("p-1.5 rounded",k==="grid"&&"bg-white dark:bg-slate-700 shadow text-blue-600 dark:text-white"),title:"Grid View",children:l.jsx(Rf,{className:"h-4 w-4"})}),l.jsx("button",{onClick:()=>S("list"),className:B("p-1.5 rounded",k==="list"&&"bg-white dark:bg-slate-700 shadow text-blue-600 dark:text-white"),title:"List View",children:l.jsx(Pf,{className:"h-4 w-4"})})]}),l.jsxs("button",{onClick:()=>h(!0),className:"flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white text-sm font-bold rounded-md shadow-sm",children:[l.jsx(zf,{className:"h-4 w-4"})," ",l.jsx("span",{className:"hidden md:inline",children:"Import"})]})]})]}),d&&l.jsx("div",{className:"fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4",children:l.jsxs("div",{className:"bg-white dark:bg-slate-900 rounded-xl shadow-2xl w-full max-w-lg border border-slate-200 dark:border-slate-800 flex flex-col max-h-[90vh]",children:[l.jsxs("div",{className:"p-4 border-b border-slate-200 dark:border-slate-800 flex justify-between items-center",children:[l.jsx("h3",{className:"font-bold text-slate-900 dark:text-white",children:"Bulk Import Contacts"}),l.jsx("button",{onClick:()=>h(!1),className:"text-slate-500 hover:text-red-500",children:l.jsx(rt,{className:"h-5 w-5"})})]}),l.jsxs("div",{className:"p-4 flex-1 overflow-y-auto",children:[l.jsxs("p",{className:"text-sm text-slate-600 dark:text-slate-400 mb-2",children:["Paste CSV data (no header). Format:",l.jsx("br",{}),l.jsx("code",{className:"bg-slate-100 dark:bg-slate-800 px-1 py-0.5 rounded text-xs",children:"Company Name, First Name, Last Name, Email, Job Title"})]}),l.jsx("textarea",{className:"w-full h-48 bg-slate-50 dark:bg-slate-950 border border-slate-300 dark:border-slate-800 rounded p-2 text-xs font-mono text-slate-800 dark:text-slate-200 focus:ring-2 focus:ring-blue-500 outline-none",placeholder:"Acme Corp, John, Doe, john@acme.com, CEO",value:N,onChange:w=>C(w.target.value)}),L&&l.jsx("div",{className:B("mt-2 text-sm font-bold p-2 rounded",L.includes("Success")?"bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400":"bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"),children:L})]}),l.jsxs("div",{className:"p-4 border-t border-slate-200 dark:border-slate-800 flex justify-end gap-2",children:[l.jsx("button",{onClick:()=>h(!1),className:"px-4 py-2 text-sm text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white",children:"Cancel"}),l.jsx("button",{onClick:U,className:"px-4 py-2 bg-blue-600 text-white text-sm font-bold rounded hover:bg-blue-500",children:"Run Import"})]})]})}),l.jsxs("div",{className:"flex-1 overflow-auto bg-slate-50 dark:bg-slate-950/30",children:[y&&l.jsx("div",{className:"p-4 text-center text-slate-500",children:"Loading contacts..."}),r.length===0&&!y?l.jsxs("div",{className:"p-12 text-center text-slate-500",children:[l.jsx(Wt,{className:"h-12 w-12 mx-auto mb-4 opacity-20"}),l.jsx("p",{className:"text-lg font-medium",children:"No contacts found"}),l.jsx("p",{className:"text-slate-400 mt-2",children:"Import a list or create one manually to get started."})]}):k==="grid"?l.jsx("div",{className:"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 p-4",children:r.map(w=>l.jsxs("div",{onClick:()=>n(w.company_id,w.id),className:"bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-lg p-4 hover:shadow-lg transition-all flex flex-col gap-3 group cursor-pointer border-l-4 border-l-slate-400",children:[l.jsxs("div",{className:"font-bold text-slate-900 dark:text-white text-sm",children:[w.title," ",w.first_name," ",w.last_name]}),l.jsx("div",{className:"text-xs text-slate-500 dark:text-slate-400",children:w.job_title||"No Title"}),l.jsxs("div",{className:"space-y-2 pt-2 border-t border-slate-100 dark:border-slate-800/50",children:[l.jsxs("div",{onClick:z=>{z.stopPropagation(),t(w.company_id)},className:"flex items-center gap-2 text-xs font-bold text-slate-600 dark:text-slate-400 hover:text-blue-500 dark:hover:text-blue-400 cursor-pointer",children:[l.jsx(Ir,{className:"h-3 w-3"})," ",w.company_name]}),l.jsxs("div",{className:"flex items-center gap-2 text-xs text-slate-500",children:[l.jsx(Tf,{className:"h-3 w-3"})," ",w.email||"-"]})]})]},w.id))}):l.jsxs("table",{className:"min-w-full divide-y divide-slate-200 dark:divide-slate-800",children:[l.jsx("thead",{className:"bg-slate-100 dark:bg-slate-950/50",children:l.jsxs("tr",{children:[l.jsx("th",{scope:"col",className:"px-3 py-3.5 text-left text-sm font-semibold text-slate-900 dark:text-white",children:"Name"}),l.jsx("th",{scope:"col",className:"px-3 py-3.5 text-left text-sm font-semibold text-slate-900 dark:text-white",children:"Company"}),l.jsx("th",{scope:"col",className:"px-3 py-3.5 text-left text-sm font-semibold text-slate-900 dark:text-white",children:"Email"}),l.jsx("th",{scope:"col",className:"px-3 py-3.5 text-left text-sm font-semibold text-slate-900 dark:text-white",children:"Role"}),l.jsx("th",{scope:"col",className:"px-3 py-3.5 text-left text-sm font-semibold text-slate-900 dark:text-white",children:"Status"})]})}),l.jsx("tbody",{className:"divide-y divide-slate-200 dark:divide-slate-800 bg-white dark:bg-slate-900",children:r.map(w=>l.jsxs("tr",{onClick:()=>n(w.company_id,w.id),className:"hover:bg-slate-50 dark:hover:bg-slate-800/50 cursor-pointer",children:[l.jsxs("td",{className:"whitespace-nowrap px-3 py-4 text-sm font-medium text-slate-900 dark:text-white",children:[w.title," ",w.first_name," ",w.last_name]}),l.jsx("td",{className:"whitespace-nowrap px-3 py-4 text-sm text-slate-500 dark:text-slate-400",children:l.jsx("div",{onClick:z=>{z.stopPropagation(),t(w.company_id)},className:"font-bold text-slate-600 dark:text-slate-400 hover:text-blue-500 dark:hover:text-blue-400 cursor-pointer",children:w.company_name})}),l.jsx("td",{className:"whitespace-nowrap px-3 py-4 text-sm text-slate-500 dark:text-slate-400",children:w.email||"-"}),l.jsx("td",{className:"whitespace-nowrap px-3 py-4 text-sm text-slate-500 dark:text-slate-400",children:w.role||"-"}),l.jsx("td",{className:"whitespace-nowrap px-3 py-4 text-sm text-slate-500 dark:text-slate-400",children:w.status||"-"})]},w.id))})]})]}),l.jsxs("div",{className:"p-3 border-t border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 flex justify-between items-center text-xs text-slate-500 dark:text-slate-400",children:[l.jsxs("span",{children:[a," Contacts total"]}),l.jsxs("div",{className:"flex gap-1 items-center",children:[l.jsx("button",{disabled:o===0,onClick:()=>u(w=>w-1),className:"p-1 rounded hover:bg-slate-100 dark:hover:bg-slate-800 disabled:opacity-30",children:l.jsx(Cf,{className:"h-4 w-4"})}),l.jsxs("span",{children:["Page ",o+1]}),l.jsx("button",{disabled:(o+1)*p>=a,onClick:()=>u(w=>w+1),className:"p-1 rounded hover:bg-slate-100 dark:hover:bg-slate-800 disabled:opacity-30",children:l.jsx(Ef,{className:"h-4 w-4"})})]})]})]})}function Cx({isOpen:e,onClose:t,onSuccess:n,apiBase:r}){const[s,a]=E.useState(""),[i,o]=E.useState(!1);if(!e)return null;const u=async()=>{var f,m;const c=s.split(` +`).map(x=>x.trim()).filter(x=>x.length>0);if(c.length!==0){o(!0);try{await D.post(`${r}/companies/bulk`,{names:c}),a(""),n(),t()}catch(x){console.error(x);const y=((m=(f=x.response)==null?void 0:f.data)==null?void 0:m.detail)||x.message||"Unknown Error";alert(`Import failed: ${y}`)}finally{o(!1)}}};return l.jsx("div",{className:"fixed inset-0 bg-black/70 backdrop-blur-sm z-50 flex items-center justify-center p-4",children:l.jsxs("div",{className:"bg-slate-900 border border-slate-700 rounded-xl w-full max-w-lg shadow-2xl",children:[l.jsxs("div",{className:"flex items-center justify-between p-4 border-b border-slate-800",children:[l.jsxs("h3",{className:"text-lg font-semibold text-white flex items-center gap-2",children:[l.jsx(Df,{className:"h-5 w-5 text-blue-400"}),"Quick Import"]}),l.jsx("button",{onClick:t,className:"text-slate-400 hover:text-white",children:l.jsx(rt,{className:"h-5 w-5"})})]}),l.jsxs("div",{className:"p-4 space-y-4",children:[l.jsx("p",{className:"text-sm text-slate-400",children:"Paste company names below (one per line). Duplicates in the database will be skipped automatically."}),l.jsx("textarea",{className:"w-full h-64 bg-slate-950 border border-slate-700 rounded-lg p-3 text-sm text-slate-200 focus:ring-2 focus:ring-blue-600 outline-none font-mono",placeholder:`Company A +Company B +Company C...`,value:s,onChange:c=>a(c.target.value)})]}),l.jsxs("div",{className:"p-4 border-t border-slate-800 flex justify-end gap-3",children:[l.jsx("button",{onClick:t,className:"px-4 py-2 text-sm font-medium text-slate-400 hover:text-white",children:"Cancel"}),l.jsx("button",{onClick:u,disabled:i||!s.trim(),className:"px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-md text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed",children:i?"Importing...":"Import Companies"})]})]})})}function Ex({contacts:e=[],initialContactId:t,onAddContact:n,onEditContact:r}){const[s,a]=E.useState(null),[i,o]=E.useState(!1);E.useEffect(()=>{if(t&&e.length>0){const y=e.find(g=>g.id===t);y&&(a({...y}),o(!0))}},[t,e]);const u={"Operativer Entscheider":"text-blue-400 border-blue-400/30 bg-blue-900/20","Infrastruktur-Verantwortlicher":"text-orange-400 border-orange-400/30 bg-orange-900/20","Wirtschaftlicher Entscheider":"text-green-400 border-green-400/30 bg-green-900/20","Innovations-Treiber":"text-purple-400 border-purple-400/30 bg-purple-900/20"},c={"":"text-slate-600 italic","Soft Denied":"text-slate-400",Bounced:"text-red-500",Redirect:"text-yellow-500",Interested:"text-green-500","Hard denied":"text-red-700",Init:"text-slate-300","1st Step":"text-blue-300","2nd Step":"text-blue-400","Not replied":"text-slate-500"},f=()=>{a({gender:"männlich",title:"",first_name:"",last_name:"",email:"",job_title:"",language:"De",role:"Operativer Entscheider",status:"",is_primary:!1}),o(!0)},m=y=>{a({...y}),o(!0)},x=()=>{s&&(s.id?r&&r(s):n&&n(s)),o(!1),a(null)};return i&&s?l.jsxs("div",{className:"bg-slate-900/50 rounded-lg p-4 border border-slate-700 space-y-4 animate-in fade-in slide-in-from-bottom-2",children:[l.jsxs("div",{className:"flex justify-between items-center border-b border-slate-700 pb-2 mb-2",children:[l.jsx("h3",{className:"text-sm font-bold text-white",children:s.id?"Edit Contact":"New Contact"}),l.jsx("button",{onClick:()=>o(!1),className:"text-slate-400 hover:text-white",children:l.jsx(rt,{className:"h-4 w-4"})})]}),l.jsxs("div",{className:"grid grid-cols-2 gap-3",children:[l.jsxs("div",{className:"space-y-1",children:[l.jsx("label",{className:"text-[10px] uppercase text-slate-500 font-bold",children:"Gender / Salutation"}),l.jsxs("select",{className:"w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:border-blue-500 outline-none",value:s.gender,onChange:y=>a({...s,gender:y.target.value}),children:[l.jsx("option",{value:"männlich",children:"Male / Herr"}),l.jsx("option",{value:"weiblich",children:"Female / Frau"})]})]}),l.jsxs("div",{className:"space-y-1",children:[l.jsx("label",{className:"text-[10px] uppercase text-slate-500 font-bold",children:"Academic Title"}),l.jsx("input",{className:"w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:border-blue-500 outline-none",value:s.title,placeholder:"e.g. Dr., Prof.",onChange:y=>a({...s,title:y.target.value})})]})]}),l.jsxs("div",{className:"grid grid-cols-2 gap-3",children:[l.jsxs("div",{className:"space-y-1",children:[l.jsx("label",{className:"text-[10px] uppercase text-slate-500 font-bold",children:"First Name"}),l.jsx("input",{className:"w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:border-blue-500 outline-none",value:s.first_name,onChange:y=>a({...s,first_name:y.target.value})})]}),l.jsxs("div",{className:"space-y-1",children:[l.jsx("label",{className:"text-[10px] uppercase text-slate-500 font-bold",children:"Last Name"}),l.jsx("input",{className:"w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:border-blue-500 outline-none",value:s.last_name,onChange:y=>a({...s,last_name:y.target.value})})]})]}),l.jsxs("div",{className:"grid grid-cols-2 gap-3",children:[l.jsxs("div",{className:"space-y-1",children:[l.jsx("label",{className:"text-[10px] uppercase text-slate-500 font-bold",children:"Email"}),l.jsx("input",{className:"w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:border-blue-500 outline-none",value:s.email,onChange:y=>a({...s,email:y.target.value})})]}),l.jsxs("div",{className:"space-y-1",children:[l.jsx("label",{className:"text-[10px] uppercase text-slate-500 font-bold",children:"Job Title (Card)"}),l.jsx("input",{className:"w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:border-blue-500 outline-none",value:s.job_title,onChange:y=>a({...s,job_title:y.target.value})})]})]}),l.jsxs("div",{className:"grid grid-cols-2 gap-3",children:[l.jsxs("div",{className:"space-y-1",children:[l.jsx("label",{className:"text-[10px] uppercase text-slate-500 font-bold",children:"Our Role Interpretation"}),l.jsx("select",{className:"w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:border-blue-500 outline-none",value:s.role,onChange:y=>a({...s,role:y.target.value}),children:Object.keys(u).map(y=>l.jsx("option",{value:y,children:y},y))})]}),l.jsxs("div",{className:"space-y-1",children:[l.jsx("label",{className:"text-[10px] uppercase text-slate-500 font-bold",children:"Marketing Status"}),l.jsxs("select",{className:"w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:border-blue-500 outline-none",value:s.status,onChange:y=>a({...s,status:y.target.value}),children:[l.jsx("option",{value:"",children:""}),Object.keys(c).filter(y=>y!=="").map(y=>l.jsx("option",{value:y,children:y},y))]})]})]}),l.jsxs("div",{className:"grid grid-cols-2 gap-3",children:[l.jsxs("div",{className:"space-y-1",children:[l.jsx("label",{className:"text-[10px] uppercase text-slate-500 font-bold",children:"Language"}),l.jsxs("select",{className:"w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:border-blue-500 outline-none",value:s.language,onChange:y=>a({...s,language:y.target.value}),children:[l.jsx("option",{value:"De",children:"De"}),l.jsx("option",{value:"En",children:"En"})]})]}),l.jsx("div",{className:"flex items-center pt-5",children:l.jsxs("label",{className:"flex items-center gap-2 cursor-pointer text-sm text-slate-300 hover:text-white",children:[l.jsx("input",{type:"checkbox",checked:s.is_primary,onChange:y=>a({...s,is_primary:y.target.checked}),className:"rounded border-slate-700 bg-slate-800 text-blue-500 focus:ring-blue-500"}),"Primary Contact"]})})]}),l.jsx("div",{className:"flex gap-2 pt-2",children:l.jsxs("button",{onClick:x,className:"flex-1 bg-blue-600 hover:bg-blue-500 text-white text-sm font-bold py-2 rounded flex items-center justify-center gap-2",children:[l.jsx(Lf,{className:"h-4 w-4"})," Save Contact"]})})]}):l.jsxs("div",{className:"space-y-4",children:[l.jsxs("div",{className:"flex items-center justify-between",children:[l.jsxs("h3",{className:"text-sm font-semibold text-slate-400 uppercase tracking-wider flex items-center gap-2",children:[l.jsx(Wt,{className:"h-4 w-4"})," Contacts List"]}),l.jsxs("button",{onClick:f,className:"flex items-center gap-1 px-3 py-1 bg-blue-600/20 text-blue-400 border border-blue-500/30 rounded hover:bg-blue-600 hover:text-white transition-all text-xs font-bold",children:[l.jsx(ni,{className:"h-3.5 w-3.5"})," ADD"]})]}),l.jsx("div",{className:"space-y-3",children:e.length===0?l.jsxs("div",{className:"p-8 rounded-xl border border-dashed border-slate-800 text-center text-slate-600",children:[l.jsx(Wt,{className:"h-8 w-8 mx-auto mb-3 opacity-20"}),l.jsx("p",{className:"text-sm font-medium",children:"No contacts yet."}),l.jsx("p",{className:"text-xs mt-1 opacity-70",children:'Click "ADD" to create the first contact for this account.'})]}):e.map(y=>l.jsxs("div",{className:B("relative bg-slate-800/30 border rounded-lg p-3 transition-all hover:bg-slate-800/50 group cursor-pointer",y.is_primary?"border-blue-500/30 shadow-lg shadow-blue-900/10":"border-slate-800"),onClick:()=>m(y),children:[y.is_primary&&l.jsx("div",{className:"absolute top-2 right-2 text-blue-500",title:"Primary Contact",children:l.jsx(kx,{className:"h-3 w-3 fill-current"})}),l.jsxs("div",{className:"flex items-start gap-3",children:[l.jsx("div",{className:"p-2 bg-slate-900 rounded-full text-slate-400 shrink-0 mt-1",children:l.jsx(Nx,{className:"h-4 w-4"})}),l.jsxs("div",{className:"flex-1 min-w-0",children:[l.jsxs("div",{className:"flex items-center gap-2 mb-0.5",children:[l.jsxs("span",{className:"text-sm font-bold text-slate-200 truncate",children:[y.title?`${y.title} `:"",y.first_name," ",y.last_name]}),l.jsx("span",{className:"text-[10px] text-slate-500 border border-slate-700 px-1 rounded",children:y.language})]}),l.jsx("div",{className:"text-xs text-slate-400 mb-2 truncate font-medium",children:y.job_title}),l.jsx("div",{className:"flex flex-wrap gap-2 mb-2",children:l.jsx("span",{className:B("text-[10px] px-1.5 py-0.5 rounded border font-medium",u[y.role]||"text-slate-400 border-slate-700"),children:y.role})}),l.jsxs("div",{className:"flex items-center gap-3 text-[10px] text-slate-500 font-mono",children:[l.jsxs("div",{className:"flex items-center gap-1 truncate",children:[l.jsx(Tf,{className:"h-3 w-3"}),y.email]}),l.jsxs("div",{className:B("flex items-center gap-1 font-bold ml-auto mr-8",c[y.status]),children:[l.jsx(ax,{className:"h-3 w-3"}),y.status||""]})]})]})]})]},y.id))})]})}function _x({companyId:e,initialContactId:t,onClose:n,apiBase:r}){var uo,co,fo;const[s,a]=E.useState(null),[i,o]=E.useState(!1),[u,c]=E.useState(!1),[f,m]=E.useState("overview"),[x,y]=E.useState(!1),[g,k]=E.useState([]),[S,p]=E.useState(""),[d,h]=E.useState(""),[N,C]=E.useState(""),[L,P]=E.useState(""),[b,U]=E.useState(""),[w,z]=E.useState("");E.useEffect(()=>{let T;return u&&(T=setInterval(()=>{Xe(!0)},2e3)),()=>clearInterval(T)},[u,e]),E.useEffect(()=>{m(t?"contacts":"overview")},[t,e]);const[W,G]=E.useState(!1),[Y,j]=E.useState(""),[I,V]=E.useState(!1),[_,M]=E.useState(""),[A,Q]=E.useState(!1),[J,Ge]=E.useState(""),[Ee,Ct]=E.useState([]),[ht,mt]=E.useState(!1),[ro,so]=E.useState(""),Xe=(T=!1)=>{if(!e)return;T||o(!0);const pe=D.get(`${r}/companies/${e}`),be=D.get(`${r}/mistakes?company_id=${e}`);Promise.all([pe,be]).then(([Ae,Nl])=>{var Xn,Yn;const Jt=Ae.data;if(a(Jt),k(Nl.data.items),u){const Gt=(Xn=Jt.enrichment_data)==null?void 0:Xn.some(jl=>jl.source_type==="wikipedia"),e0=(Yn=Jt.enrichment_data)==null?void 0:Yn.some(jl=>jl.source_type==="ai_analysis");(Gt&&Jt.status==="DISCOVERED"||e0&&Jt.status==="ENRICHED")&&c(!1)}}).catch(console.error).finally(()=>{T||o(!1)})};E.useEffect(()=>{Xe(),G(!1),V(!1),Q(!1),mt(!1),c(!1),D.get(`${r}/industries`).then(T=>Ct(T.data)).catch(console.error)},[e]);const Ff=async()=>{if(e){c(!0);try{await D.post(`${r}/enrich/discover`,{company_id:e})}catch(T){console.error(T),c(!1)}}},If=async()=>{if(e){c(!0);try{await D.post(`${r}/enrich/analyze`,{company_id:e})}catch(T){console.error(T),c(!1)}}},Uf=()=>{if(!s)return;const T={metadata:{id:s.id,exported_at:new Date().toISOString(),source:"Company Explorer (Robotics Edition)"},company:{name:s.name,website:s.website,status:s.status,industry_ai:s.industry_ai,created_at:s.created_at},quantitative_potential:{calculated_metric_name:s.calculated_metric_name,calculated_metric_value:s.calculated_metric_value,calculated_metric_unit:s.calculated_metric_unit,standardized_metric_value:s.standardized_metric_value,standardized_metric_unit:s.standardized_metric_unit,metric_source:s.metric_source,metric_source_url:s.metric_source_url,metric_proof_text:s.metric_proof_text,metric_confidence:s.metric_confidence,metric_confidence_reason:s.metric_confidence_reason},enrichment:s.enrichment_data,signals:s.signals},pe=new Blob([JSON.stringify(T,null,2)],{type:"application/json"}),be=URL.createObjectURL(pe),Ae=document.createElement("a");Ae.href=be,Ae.download=`company-export-${s.id}-${s.name.replace(/[^a-z0-9]/gi,"_").toLowerCase()}.json`,document.body.appendChild(Ae),Ae.click(),document.body.removeChild(Ae),URL.revokeObjectURL(be)},$f=async()=>{if(e){c(!0);try{await D.post(`${r}/companies/${e}/override/wiki?url=${encodeURIComponent(Y)}`),G(!1),Xe()}catch(T){alert("Update failed"),console.error(T)}finally{c(!1)}}},Vf=async()=>{if(e){c(!0);try{await D.post(`${r}/companies/${e}/override/website?url=${encodeURIComponent(_)}`),V(!1),Xe()}catch(T){alert("Update failed"),console.error(T)}finally{c(!1)}}},Bf=async()=>{if(e){c(!0);try{await D.post(`${r}/companies/${e}/override/impressum?url=${encodeURIComponent(J)}`),Q(!1),Xe()}catch(T){alert("Impressum update failed"),console.error(T)}finally{c(!1)}}},Hf=async()=>{if(e){c(!0);try{await D.put(`${r}/companies/${e}/industry`,{industry_ai:ro}),mt(!1),Xe()}catch(T){alert("Industry update failed"),console.error(T)}finally{c(!1)}}},Wf=async()=>{if(e){c(!0);try{await D.post(`${r}/companies/${e}/reevaluate-wikipedia`)}catch(T){console.error(T),c(!1)}}},Qf=async()=>{var T,pe;if(e&&window.confirm(`Are you sure you want to delete "${s==null?void 0:s.name}"? This action cannot be undone.`))try{await D.delete(`${r}/companies/${e}`),n(),window.location.reload()}catch(be){alert("Failed to delete company: "+(((pe=(T=be.response)==null?void 0:T.data)==null?void 0:pe.detail)||be.message))}},lo=async(T,pe)=>{if(e)try{await D.post(`${r}/enrichment/${e}/${T}/lock?locked=${!pe}`),Xe(!0)}catch(be){console.error("Lock toggle failed",be)}},qf=async()=>{if(e){if(!S){alert("Field Name is required.");return}c(!0);try{const T={field_name:S,wrong_value:d||null,corrected_value:N||null,source_url:L||null,quote:b||null,user_comment:w||null};await D.post(`${r}/companies/${e}/report-mistake`,T),alert("Mistake reported successfully!"),y(!1),p(""),h(""),C(""),P(""),U(""),z(""),Xe(!0)}catch(T){alert("Failed to report mistake."),console.error(T)}finally{c(!1)}}},Kf=async T=>{if(e)try{await D.post(`${r}/contacts`,{...T,company_id:e}),Xe(!0)}catch(pe){alert("Failed to add contact"),console.error(pe)}},Jf=async T=>{if(T.id)try{await D.put(`${r}/contacts/${T.id}`,T),Xe(!0)}catch(pe){alert("Failed to update contact"),console.error(pe)}};if(!e)return null;const _e=(uo=s==null?void 0:s.enrichment_data)==null?void 0:uo.find(T=>T.source_type==="wikipedia"),oe=_e==null?void 0:_e.content,Gf=_e==null?void 0:_e.is_locked,ao=_e==null?void 0:_e.created_at,mn=(co=s==null?void 0:s.enrichment_data)==null?void 0:co.find(T=>T.source_type==="ai_analysis"),Jr=mn==null?void 0:mn.content,io=mn==null?void 0:mn.created_at,Ye=(fo=s==null?void 0:s.enrichment_data)==null?void 0:fo.find(T=>T.source_type==="website_scrape"),bl=Ye==null?void 0:Ye.content,ze=bl==null?void 0:bl.impressum,oo=Ye==null?void 0:Ye.created_at,Xf=()=>{if(!(s!=null&&s.industry_details))return null;const{pains:T,gains:pe,priority:be,notes:Ae}=s.industry_details;return l.jsxs("div",{className:"bg-purple-50 dark:bg-purple-900/10 rounded-xl p-5 border border-purple-100 dark:border-purple-900/50 mb-6",children:[l.jsxs("h3",{className:"text-sm font-semibold text-purple-700 dark:text-purple-300 uppercase tracking-wider mb-3 flex items-center gap-2",children:[l.jsx(Of,{className:"h-4 w-4"})," Strategic Fit (Notion)"]}),l.jsxs("div",{className:"grid gap-4",children:[l.jsxs("div",{className:"flex items-center gap-2",children:[l.jsx("span",{className:"text-xs text-slate-500 font-bold uppercase",children:"Status:"}),l.jsx("span",{className:B("px-2 py-0.5 rounded text-xs font-bold",be==="Freigegeben"?"bg-green-100 text-green-700":"bg-yellow-100 text-yellow-700"),children:be||"N/A"})]}),T&&l.jsxs("div",{children:[l.jsx("div",{className:"text-[10px] text-red-600 dark:text-red-400 uppercase font-bold tracking-tight mb-1",children:"Pain Points"}),l.jsx("div",{className:"text-sm text-slate-700 dark:text-slate-300 whitespace-pre-line",children:T})]}),pe&&l.jsxs("div",{children:[l.jsx("div",{className:"text-[10px] text-green-600 dark:text-green-400 uppercase font-bold tracking-tight mb-1",children:"Gain Points"}),l.jsx("div",{className:"text-sm text-slate-700 dark:text-slate-300 whitespace-pre-line",children:pe})]}),Ae&&l.jsxs("div",{className:"pt-2 border-t border-purple-200 dark:border-purple-800",children:[l.jsx("div",{className:"text-[10px] text-purple-500 uppercase font-bold tracking-tight",children:"Internal Notes"}),l.jsx("div",{className:"text-xs text-slate-500 italic",children:Ae})]})]})]})},Yf=()=>!(s!=null&&s.ai_opener)&&!(s!=null&&s.ai_opener_secondary)?null:l.jsxs("div",{className:"bg-orange-50 dark:bg-orange-900/10 rounded-xl p-5 border border-orange-100 dark:border-orange-900/50 mb-6",children:[l.jsxs("h3",{className:"text-sm font-semibold text-orange-700 dark:text-orange-300 uppercase tracking-wider mb-3 flex items-center gap-2",children:[l.jsx(Pt,{className:"h-4 w-4"})," Marketing AI (Openers)"]}),l.jsxs("div",{className:"space-y-4",children:[s.ai_opener&&l.jsxs("div",{className:"p-3 bg-white dark:bg-slate-900 rounded border border-orange-200 dark:border-orange-800",children:[l.jsx("div",{className:"flex justify-between items-center mb-1",children:l.jsx("div",{className:"text-[10px] text-orange-600 dark:text-orange-400 uppercase font-bold tracking-tight",children:"Primary: Infrastructure/Cleaning"})}),l.jsxs("div",{className:"text-sm text-slate-700 dark:text-slate-200 leading-relaxed italic",children:['"',s.ai_opener,'"']})]}),s.ai_opener_secondary&&l.jsxs("div",{className:"p-3 bg-white dark:bg-slate-900 rounded border border-orange-200 dark:border-orange-800",children:[l.jsx("div",{className:"flex justify-between items-center mb-1",children:l.jsx("div",{className:"text-[10px] text-orange-600 dark:text-orange-400 uppercase font-bold tracking-tight",children:"Secondary: Service/Logistics"})}),l.jsxs("div",{className:"text-sm text-slate-700 dark:text-slate-200 leading-relaxed italic",children:['"',s.ai_opener_secondary,'"']})]}),l.jsx("p",{className:"text-[10px] text-slate-500 text-center",children:'These sentences are statically pre-calculated for the "First Sentence Matching" strategy.'})]})]}),Zf=()=>{if(!s)return null;const T=s.crm_name||s.crm_website,pe=s.confidence_score!=null||s.data_mismatch_score!=null;if(!T&&!pe)return null;const be=s.confidence_score??0,Ae=s.data_mismatch_score??0,Nl=Gt=>Gt>.8?{bg:"bg-green-100",text:"text-green-700"}:Gt>.5?{bg:"bg-yellow-100",text:"text-yellow-700"}:{bg:"bg-red-100",text:"text-red-700"},Jt=Gt=>Gt<=.3?{bg:"bg-green-100",text:"text-green-700"}:Gt<=.5?{bg:"bg-yellow-100",text:"text-yellow-700"}:{bg:"bg-red-100",text:"text-red-700"},Xn=Nl(be),Yn=Jt(Ae);return l.jsxs("div",{className:"bg-slate-50 dark:bg-slate-950 rounded-lg p-4 border border-slate-200 dark:border-slate-800 mb-6",children:[l.jsx("div",{className:"flex justify-between items-center mb-4",children:l.jsxs("h3",{className:"text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider flex items-center gap-2",children:[l.jsx(vx,{className:"h-4 w-4"})," Data Quality & CRM Sync"]})}),l.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-4 text-xs mb-4",children:[l.jsxs("div",{className:"space-y-3 p-3 bg-white dark:bg-slate-900 rounded border border-slate-200 dark:border-slate-800",children:[l.jsx("div",{className:"text-[10px] font-bold text-slate-400 uppercase",children:"AI Quality Scores"}),s.confidence_score!=null&&l.jsxs("div",{className:"flex items-center justify-between",children:[l.jsx("span",{className:"text-slate-500 dark:text-slate-400",children:"Classification Confidence"}),l.jsxs("span",{className:B("font-bold px-2 py-0.5 rounded text-xs",Xn.bg,Xn.text),children:[(be*100).toFixed(0),"%"]})]}),s.data_mismatch_score!=null&&l.jsxs("div",{className:"flex items-center justify-between",children:[l.jsx("span",{className:"text-slate-500 dark:text-slate-400",children:"CRM Data Match"}),l.jsxs("span",{className:B("font-bold px-2 py-0.5 rounded text-xs",Yn.bg,Yn.text),children:[((1-Ae)*100).toFixed(0),"%"]})]})]}),l.jsxs("div",{className:"p-3 bg-slate-100 dark:bg-slate-800/50 rounded",children:[l.jsx("div",{className:"text-[10px] font-bold text-slate-400 uppercase mb-2",children:"SuperOffice (CRM)"}),l.jsxs("div",{className:"space-y-2",children:[l.jsxs("div",{children:[l.jsx("span",{className:"text-slate-400",children:"Name:"})," ",l.jsx("span",{className:"font-medium break-all",children:s.crm_name||"-"})]}),l.jsxs("div",{children:[l.jsx("span",{className:"text-slate-400",children:"Web:"})," ",l.jsx("span",{className:"font-mono break-all",children:s.crm_website||"-"})]})]})]})]}),l.jsx("div",{className:"text-xs",children:l.jsxs("div",{className:"p-3 bg-white dark:bg-slate-900 rounded border border-blue-100 dark:border-blue-900/50",children:[l.jsx("div",{className:"text-[10px] font-bold text-blue-500 uppercase mb-2",children:"Enriched Data (AI)"}),l.jsxs("div",{className:"space-y-2",children:[l.jsxs("div",{children:[l.jsx("span",{className:"text-slate-400",children:"Name:"})," ",l.jsx("span",{className:"font-medium text-slate-900 dark:text-white",children:s.name})]}),l.jsxs("div",{children:[l.jsx("span",{className:"text-slate-400",children:"Web:"})," ",l.jsx("span",{className:"font-mono text-blue-600 dark:text-blue-400",children:s.website})]})]})]})})]})};return l.jsx("div",{className:"fixed inset-y-0 right-0 w-full md:w-[600px] bg-white dark:bg-slate-900 border-l border-slate-200 dark:border-slate-800 shadow-2xl transform transition-transform duration-300 ease-in-out z-50 overflow-y-auto",children:i?l.jsx("div",{className:"p-8 text-slate-500",children:"Loading details..."}):s?l.jsxs("div",{className:"flex flex-col h-full",children:[l.jsxs("div",{className:"p-6 border-b border-slate-200 dark:border-slate-800 bg-slate-50/80 dark:bg-slate-950/50 backdrop-blur-sm sticky top-0 z-10",children:[l.jsxs("div",{className:"flex justify-between items-start mb-4",children:[l.jsx("h2",{className:"text-xl font-bold text-slate-900 dark:text-white leading-tight",children:s.name}),l.jsxs("div",{className:"flex items-center gap-2",children:[l.jsx("button",{onClick:Qf,className:"p-1.5 text-slate-500 hover:text-red-600 dark:hover:text-red-500 transition-colors",title:"Delete Company",children:l.jsx(Mf,{className:"h-4 w-4"})}),l.jsx("button",{onClick:Uf,className:"p-1.5 text-slate-500 hover:text-blue-600 dark:hover:text-blue-400 transition-colors",title:"Export JSON",children:l.jsx(_f,{className:"h-4 w-4"})}),l.jsx("button",{onClick:()=>y(!0),className:"p-1.5 text-slate-500 hover:text-orange-600 dark:hover:text-orange-500 transition-colors",title:"Report a Mistake",children:l.jsx(el,{className:"h-4 w-4"})}),l.jsx("button",{onClick:()=>Xe(!0),className:"p-1.5 text-slate-500 hover:text-slate-900 dark:hover:text-white transition-colors",title:"Refresh",children:l.jsx(ri,{className:B("h-4 w-4",(i||u)&&"animate-spin")})}),l.jsx("button",{onClick:n,className:"p-1.5 text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors",children:l.jsx(rt,{className:"h-6 w-6"})})]})]}),l.jsx("div",{className:"flex flex-wrap gap-2 text-sm items-center",children:I?l.jsxs("div",{className:"flex items-center gap-1 animate-in fade-in zoom-in duration-200",children:[l.jsx("input",{type:"text",value:_,onChange:T=>M(T.target.value),placeholder:"https://...",className:"bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-700 rounded px-2 py-0.5 text-[10px] text-slate-900 dark:text-white focus:ring-1 focus:ring-blue-500 outline-none w-48",autoFocus:!0}),l.jsx("button",{onClick:Vf,className:"p-1 bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-400 rounded hover:bg-green-200 dark:hover:bg-green-900 transition-colors",children:l.jsx(Pn,{className:"h-3 w-3"})}),l.jsx("button",{onClick:()=>V(!1),className:"p-1 text-slate-500 hover:text-red-500 transition-colors",children:l.jsx(rt,{className:"h-3 w-3"})})]}):l.jsxs("div",{className:"flex items-center gap-2 group",children:[s.website&&s.website!=="k.A."?l.jsxs("a",{href:s.website.startsWith("http")?s.website:`https://${s.website}`,target:"_blank",rel:"noopener noreferrer",className:"flex items-center gap-1 text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 transition-colors font-medium",children:[l.jsx(fr,{className:"h-3.5 w-3.5"})," ",new URL(s.website.startsWith("http")?s.website:`https://${s.website}`).hostname.replace("www.","")]}):l.jsx("span",{className:"text-slate-500 italic text-xs",children:"No website"}),l.jsx("button",{onClick:()=>{M(s.website&&s.website!=="k.A."?s.website:""),V(!0)},className:"p-1 text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors opacity-0 group-hover:opacity-100",title:"Edit Website URL",children:l.jsx(na,{className:"h-3 w-3"})})]})}),g.length>0&&l.jsxs("div",{className:"mt-4 p-4 bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800/50 rounded-lg",children:[l.jsxs("h4",{className:"flex items-center gap-2 text-sm font-bold text-orange-800 dark:text-orange-300 mb-3",children:[l.jsx(ix,{className:"h-4 w-4"}),"Existing Correction Proposals"]}),l.jsx("div",{className:"space-y-3 max-h-40 overflow-y-auto pr-2",children:g.map(T=>l.jsxs("div",{className:"text-xs p-3 bg-white dark:bg-slate-800/50 rounded border border-slate-200 dark:border-slate-700/50",children:[l.jsxs("div",{className:"flex justify-between items-start",children:[l.jsx("span",{className:"font-bold text-slate-800 dark:text-slate-200",children:T.field_name}),l.jsx("span",{className:B("px-2 py-0.5 rounded-full text-[9px] font-medium",{"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-300":T.status==="PENDING","bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-300":T.status==="APPROVED","bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300":T.status==="REJECTED"}),children:T.status})]}),l.jsxs("p",{className:"text-slate-600 dark:text-slate-400 mt-1",children:[l.jsx("span",{className:"line-through text-red-500/80",children:T.wrong_value||"N/A"})," → ",l.jsx("strong",{className:"text-green-600 dark:text-green-400",children:T.corrected_value||"N/A"})]}),T.user_comment&&l.jsxs("p",{className:"mt-2 text-slate-500 italic",children:['"',T.user_comment,'"']})]},T.id))})]}),x&&l.jsxs("div",{className:"mt-4 p-4 bg-slate-100 dark:bg-slate-800 rounded border border-slate-200 dark:border-slate-700 animate-in slide-in-from-top-2",children:[l.jsxs("h4",{className:"text-sm font-bold mb-3 flex justify-between items-center",children:["Report a Data Error",l.jsx("button",{onClick:()=>y(!1),className:"text-slate-400 hover:text-red-500",children:l.jsx(rt,{className:"h-4 w-4"})})]}),l.jsxs("div",{className:"space-y-3",children:[l.jsxs("div",{children:[l.jsx("label",{className:"block text-[10px] uppercase font-bold text-slate-500 mb-1",children:"Field Name (Required)"}),l.jsx("input",{className:"w-full text-xs p-2 rounded border dark:bg-slate-900 dark:border-slate-700",value:S,onChange:T=>p(T.target.value),placeholder:"e.g. Revenue, Employee Count"})]}),l.jsxs("div",{className:"grid grid-cols-2 gap-2",children:[l.jsxs("div",{children:[l.jsx("label",{className:"block text-[10px] uppercase font-bold text-slate-500 mb-1",children:"Wrong Value"}),l.jsx("input",{className:"w-full text-xs p-2 rounded border dark:bg-slate-900 dark:border-slate-700",value:d,onChange:T=>h(T.target.value)})]}),l.jsxs("div",{children:[l.jsx("label",{className:"block text-[10px] uppercase font-bold text-slate-500 mb-1",children:"Correct Value"}),l.jsx("input",{className:"w-full text-xs p-2 rounded border dark:bg-slate-900 dark:border-slate-700",value:N,onChange:T=>C(T.target.value)})]})]}),l.jsxs("div",{children:[l.jsx("label",{className:"block text-[10px] uppercase font-bold text-slate-500 mb-1",children:"Source URL / Proof"}),l.jsx("input",{className:"w-full text-xs p-2 rounded border dark:bg-slate-900 dark:border-slate-700",value:L,onChange:T=>P(T.target.value)})]}),l.jsxs("div",{children:[l.jsx("label",{className:"block text-[10px] uppercase font-bold text-slate-500 mb-1",children:"Comment"}),l.jsx("textarea",{className:"w-full text-xs p-2 rounded border dark:bg-slate-900 dark:border-slate-700",rows:2,value:w,onChange:T=>z(T.target.value)})]}),l.jsx("button",{onClick:qf,disabled:u,className:"w-full bg-orange-600 hover:bg-orange-700 text-white py-2 rounded text-xs font-bold",children:"SUBMIT REPORT"})]})]}),l.jsxs("div",{className:"mt-6 flex border-b border-slate-200 dark:border-slate-800",children:[l.jsx("button",{onClick:()=>m("overview"),className:B("px-4 py-2 text-sm font-medium transition-colors border-b-2",f==="overview"?"border-blue-500 text-blue-600 dark:text-blue-400":"border-transparent text-slate-500 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200"),children:"Overview"}),l.jsxs("button",{onClick:()=>m("contacts"),className:B("px-4 py-2 text-sm font-medium transition-colors border-b-2 flex items-center gap-2",f==="contacts"?"border-blue-500 text-blue-600 dark:text-blue-400":"border-transparent text-slate-500 dark:text-slate-400 hover:text-slate-800 dark:hover:text-slate-200"),children:["Contacts",s.contacts&&s.contacts.length>0&&l.jsx("span",{className:"bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-300 px-1.5 py-0.5 rounded-full text-[10px] min-w-[1.25rem] text-center",children:s.contacts.length})]})]})]}),l.jsxs("div",{className:"p-6 space-y-8 bg-white dark:bg-slate-900",children:[f==="overview"&&l.jsxs(l.Fragment,{children:[l.jsxs("div",{className:"flex gap-2 mb-6",children:[l.jsxs("button",{onClick:Ff,disabled:u,className:"flex-1 flex items-center justify-center gap-2 bg-white dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-700 disabled:opacity-50 text-slate-700 dark:text-white text-xs font-bold py-2 rounded-md border border-slate-200 dark:border-slate-700 transition-all shadow-sm",children:[l.jsx(An,{className:"h-3.5 w-3.5"}),u?"Processing...":"DISCOVER"]}),l.jsxs("button",{onClick:If,disabled:u||!s.website||s.website==="k.A.",className:"flex-1 flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white text-xs font-bold py-2 rounded-md transition-all shadow-lg shadow-blue-900/20",children:[l.jsx(Pt,{className:"h-3.5 w-3.5"}),u?"Analyzing...":"ANALYZE POTENTIAL"]})]}),Zf(),Xf(),Yf(),l.jsxs("div",{className:"bg-slate-50 dark:bg-slate-950 rounded-lg p-4 border border-slate-200 dark:border-slate-800 flex flex-col gap-2",children:[l.jsxs("div",{className:"flex items-center justify-between mb-1",children:[l.jsxs("div",{className:"flex items-center gap-2",children:[l.jsx("div",{className:"p-1 bg-white dark:bg-slate-800 rounded text-slate-400",children:l.jsx(ea,{className:"h-3 w-3"})}),l.jsx("span",{className:"text-[10px] uppercase font-bold text-slate-500 tracking-wider",children:"Official Legal Data"})]}),l.jsxs("div",{className:"flex items-center gap-2",children:[oo&&l.jsxs("div",{className:"text-[10px] text-slate-500 flex items-center gap-1",children:[l.jsx(ta,{className:"h-3 w-3"})," ",new Date(oo).toLocaleDateString()]}),Ye&&l.jsx("button",{onClick:()=>lo("website_scrape",Ye.is_locked||!1),className:B("p-1 rounded transition-colors",Ye.is_locked?"text-green-600 dark:text-green-400 hover:text-green-700":"text-slate-400 hover:text-slate-900 dark:hover:text-white"),title:Ye.is_locked?"Data Locked":"Unlocked",children:Ye.is_locked?l.jsx(Iu,{className:"h-3.5 w-3.5"}):l.jsx($u,{className:"h-3.5 w-3.5"})}),A?l.jsxs("div",{className:"flex items-center gap-1 animate-in fade-in zoom-in duration-200",children:[l.jsx("button",{onClick:Bf,className:"p-1 bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400 rounded hover:bg-green-200 dark:hover:bg-green-900 transition-colors",children:l.jsx(Pn,{className:"h-3 w-3"})}),l.jsx("button",{onClick:()=>Q(!1),className:"p-1 text-slate-500 hover:text-red-500 transition-colors",children:l.jsx(rt,{className:"h-3 w-3"})})]}):l.jsx("button",{onClick:()=>{Ge(""),Q(!0)},className:"p-1 text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors",title:"Set Impressum URL Manually",children:l.jsx(na,{className:"h-3 w-3"})})]})]}),A&&l.jsx("div",{className:"mb-2 animate-in slide-in-from-top-1 duration-200",children:l.jsx("input",{type:"text",value:J,onChange:T=>Ge(T.target.value),placeholder:"https://.../impressum",className:"w-full bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 rounded px-2 py-1 text-xs text-slate-900 dark:text-white focus:ring-1 focus:ring-blue-500 outline-none",autoFocus:!0})}),ze?l.jsxs(l.Fragment,{children:[l.jsx("div",{className:"text-sm font-medium text-slate-900 dark:text-white",children:ze.legal_name||"Unknown Legal Name"}),l.jsxs("div",{className:"flex items-start gap-2 text-xs text-slate-500 dark:text-slate-400",children:[l.jsx(ti,{className:"h-3 w-3 mt-0.5 shrink-0"}),l.jsxs("div",{children:[l.jsx("div",{children:ze.street}),l.jsxs("div",{children:[ze.zip," ",ze.city]})]})]}),(ze.email||ze.phone)&&l.jsxs("div",{className:"mt-2 pt-2 border-t border-slate-200 dark:border-slate-900 flex flex-wrap gap-4 text-[10px] text-slate-500 font-mono",children:[ze.email&&l.jsx("span",{children:ze.email}),ze.phone&&l.jsx("span",{children:ze.phone}),ze.vat_id&&l.jsxs("span",{className:"text-blue-600 dark:text-blue-400/80",children:["VAT: ",ze.vat_id]})]})]}):!A&&l.jsx("div",{className:"text-[10px] text-slate-500 italic py-2",children:"No legal data found. Click pencil to provide direct Impressum link."})]}),l.jsx("div",{className:"bg-blue-50/50 dark:bg-blue-900/10 rounded-xl p-5 border border-blue-100 dark:border-blue-900/50 mb-6",children:l.jsxs("div",{className:"grid grid-cols-2 gap-6",children:[l.jsxs("div",{children:[l.jsx("div",{className:"text-[10px] text-blue-600 dark:text-blue-400 uppercase font-bold tracking-tight mb-2",children:"Industry Focus"}),ht?l.jsxs("div",{className:"space-y-2",children:[l.jsxs("select",{value:ro,onChange:T=>so(T.target.value),className:"w-full bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-700 rounded px-2 py-1.5 text-sm text-slate-900 dark:text-white focus:ring-1 focus:ring-blue-500 outline-none",autoFocus:!0,children:[l.jsx("option",{value:"Others",children:"Others"}),Ee.map(T=>l.jsx("option",{value:T.name,children:T.name},T.id))]}),l.jsxs("div",{className:"flex gap-2",children:[l.jsxs("button",{onClick:Hf,className:"flex-1 px-3 py-1.5 bg-blue-600 text-white rounded text-xs font-medium hover:bg-blue-700 transition-colors flex items-center justify-center gap-2",children:[l.jsx(Pn,{className:"h-3.5 w-3.5"})," Save & Re-Extract"]}),l.jsx("button",{onClick:()=>mt(!1),className:"px-3 py-1.5 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded text-xs font-medium hover:bg-slate-300 dark:hover:bg-slate-600 transition-colors",children:l.jsx(rt,{className:"h-3.5 w-3.5"})})]})]}):l.jsxs("div",{className:"flex items-center gap-3",children:[l.jsx("div",{className:"p-2 bg-white dark:bg-slate-800 rounded-lg shadow-sm",children:l.jsx(ea,{className:"h-5 w-5 text-blue-600 dark:text-blue-400"})}),l.jsxs("div",{children:[l.jsx("div",{className:"text-sm font-semibold text-slate-900 dark:text-white",children:s.industry_ai||"Not Classified"}),l.jsx("button",{onClick:()=>{so(s.industry_ai||"Others"),mt(!0)},className:"text-xs text-blue-600 dark:text-blue-400 hover:underline",children:"Change Industry & Re-Extract"})]})]})]}),l.jsxs("div",{children:[l.jsx("div",{className:"text-[10px] text-slate-500 uppercase font-bold tracking-tight mb-2",children:"Analysis Status"}),l.jsxs("div",{className:"flex items-center gap-3",children:[l.jsx("div",{className:"p-2 bg-white dark:bg-slate-800 rounded-lg shadow-sm",children:l.jsx(Pt,{className:"h-5 w-5 text-slate-500"})}),l.jsx("div",{className:B("px-3 py-1 rounded-full text-xs font-bold",s.status==="ENRICHED"?"bg-green-100 text-green-700 border border-green-200":s.status==="DISCOVERED"?"bg-blue-100 text-blue-700 border border-blue-200":"bg-slate-100 text-slate-600 border border-slate-200"),children:s.status})]})]})]})}),Jr&&l.jsxs("div",{className:"space-y-4",children:[l.jsxs("div",{className:"flex items-center justify-between",children:[l.jsxs("h3",{className:"text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider flex items-center gap-2",children:[l.jsx(Pt,{className:"h-4 w-4"})," AI Strategic Dossier"]}),io&&l.jsxs("div",{className:"text-[10px] text-slate-500 flex items-center gap-1",children:[l.jsx(ta,{className:"h-3 w-3"})," ",new Date(io).toLocaleDateString()]})]}),l.jsxs("div",{className:"bg-white dark:bg-slate-800/30 rounded-xl p-5 border border-slate-200 dark:border-slate-800/50 space-y-4 shadow-sm",children:[l.jsxs("div",{children:[l.jsx("div",{className:"text-[10px] text-blue-600 dark:text-blue-400 uppercase font-bold tracking-tight mb-1",children:"Business Model"}),l.jsx("p",{className:"text-sm text-slate-700 dark:text-slate-200 leading-relaxed",children:Jr.business_model||"No summary available."})]}),Jr.infrastructure_evidence&&l.jsxs("div",{className:"pt-4 border-t border-slate-200 dark:border-slate-800/50",children:[l.jsx("div",{className:"text-[10px] text-orange-600 dark:text-orange-400 uppercase font-bold tracking-tight mb-1",children:"Infrastructure Evidence"}),l.jsxs("p",{className:"text-sm text-slate-600 dark:text-slate-300 italic leading-relaxed",children:['"',Jr.infrastructure_evidence,'"']})]})]})]}),l.jsxs("div",{className:"space-y-4",children:[l.jsxs("div",{className:"flex items-center justify-between",children:[l.jsxs("h3",{className:"text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider flex items-center gap-2",children:[l.jsx(fr,{className:"h-4 w-4"})," Company Profile (Wikipedia)"]}),l.jsxs("div",{className:"flex items-center gap-2",children:[ao&&l.jsxs("div",{className:"text-[10px] text-slate-500 flex items-center gap-1 mr-2",children:[l.jsx(ta,{className:"h-3 w-3"})," ",new Date(ao).toLocaleDateString()]}),_e&&l.jsx("button",{onClick:()=>lo("wikipedia",_e.is_locked||!1),className:B("p-1 rounded transition-colors mr-1",_e.is_locked?"text-green-600 dark:text-green-400 hover:text-green-700":"text-slate-400 hover:text-slate-900 dark:hover:text-white"),title:_e.is_locked?"Wiki Data Locked":"Wiki Data Unlocked",children:_e.is_locked?l.jsx(Iu,{className:"h-3.5 w-3.5"}):l.jsx($u,{className:"h-3.5 w-3.5"})}),l.jsx("button",{onClick:Wf,disabled:u,className:"p-1 text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors disabled:opacity-50",title:"Re-run metric extraction from Wikipedia text",children:l.jsx(ri,{className:B("h-3.5 w-3.5",u&&"animate-spin")})}),W?l.jsxs("div",{className:"flex items-center gap-1",children:[l.jsx("button",{onClick:$f,className:"p-1 bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400 rounded hover:bg-green-200 dark:hover:bg-green-900 transition-colors",title:"Save & Rescan",children:l.jsx(Pn,{className:"h-3.5 w-3.5"})}),l.jsx("button",{onClick:()=>G(!1),className:"p-1 text-slate-500 hover:text-red-500 transition-colors",title:"Cancel",children:l.jsx(rt,{className:"h-3.5 w-3.5"})})]}):l.jsx("button",{onClick:()=>{j((oe==null?void 0:oe.url)||""),G(!0)},className:"p-1 text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors",title:"Edit / Override URL",children:l.jsx(na,{className:"h-3.5 w-3.5"})})]})]}),W&&l.jsxs("div",{className:"mb-2",children:[l.jsx("input",{type:"text",value:Y,onChange:T=>j(T.target.value),placeholder:"Paste Wikipedia URL here...",className:"w-full bg-white dark:bg-slate-800 border border-slate-300 dark:border-slate-700 rounded px-2 py-1 text-sm text-slate-900 dark:text-white focus:ring-1 focus:ring-blue-500 outline-none"}),l.jsx("p",{className:"text-[10px] text-slate-500 mt-1",children:"Paste a valid URL. Saving will trigger a re-scan."})]}),oe&&oe.url!=="k.A."&&!W?l.jsx("div",{children:l.jsxs("div",{className:"bg-white dark:bg-slate-800/30 rounded-xl p-5 border border-slate-200 dark:border-slate-800/50 relative overflow-hidden shadow-sm",children:[l.jsx("div",{className:"absolute top-0 right-0 p-3 opacity-10",children:l.jsx(fr,{className:"h-16 w-16 text-slate-900 dark:text-white"})}),Gf&&l.jsxs("div",{className:"absolute top-2 right-2 flex items-center gap-1 px-1.5 py-0.5 bg-yellow-100 dark:bg-yellow-900/30 border border-yellow-200 dark:border-yellow-800/50 rounded text-[9px] text-yellow-600 dark:text-yellow-500",children:[l.jsx(si,{className:"h-2.5 w-2.5"})," Manual Override"]}),l.jsxs("p",{className:"text-sm text-slate-600 dark:text-slate-300 leading-relaxed italic mb-4",children:['"',oe.first_paragraph,'"']}),l.jsxs("div",{className:"grid grid-cols-2 gap-y-4 gap-x-6",children:[l.jsxs("div",{className:"flex items-center gap-3",children:[l.jsx("div",{className:"p-2 bg-slate-100 dark:bg-slate-900 rounded-lg text-blue-500",children:l.jsx(Wt,{className:"h-4 w-4"})}),l.jsxs("div",{children:[l.jsx("div",{className:"text-[10px] text-slate-500 uppercase font-bold tracking-tight",children:"Employees"}),l.jsx("div",{className:"text-sm text-slate-700 dark:text-slate-200 font-medium",children:oe.mitarbeiter||"k.A."})]})]}),l.jsxs("div",{className:"flex items-center gap-3",children:[l.jsx("div",{className:"p-2 bg-slate-100 dark:bg-slate-900 rounded-lg text-green-500",children:l.jsx(fx,{className:"h-4 w-4"})}),l.jsxs("div",{children:[l.jsx("div",{className:"text-[10px] text-slate-500 uppercase font-bold tracking-tight",children:"Revenue"}),l.jsx("div",{className:"text-sm text-slate-700 dark:text-slate-200 font-medium",children:oe.umsatz&&oe.umsatz!=="k.A."?`${oe.umsatz} Mio. €`:"k.A."})]})]}),l.jsxs("div",{className:"flex items-center gap-3",children:[l.jsx("div",{className:"p-2 bg-slate-100 dark:bg-slate-900 rounded-lg text-orange-500",children:l.jsx(ti,{className:"h-4 w-4"})}),l.jsxs("div",{children:[l.jsx("div",{className:"text-[10px] text-slate-500 uppercase font-bold tracking-tight",children:"Headquarters"}),l.jsxs("div",{className:"text-sm text-slate-700 dark:text-slate-200 font-medium",children:[oe.sitz_stadt,oe.sitz_land?`, ${oe.sitz_land}`:""]})]})]}),l.jsxs("div",{className:"flex items-center gap-3",children:[l.jsx("div",{className:"p-2 bg-slate-100 dark:bg-slate-900 rounded-lg text-purple-500",children:l.jsx(ea,{className:"h-4 w-4"})}),l.jsxs("div",{children:[l.jsx("div",{className:"text-[10px] text-slate-500 uppercase font-bold tracking-tight",children:"Wiki Industry"}),l.jsx("div",{className:"text-sm text-slate-700 dark:text-slate-200 font-medium truncate max-w-[150px]",title:oe.branche,children:oe.branche||"k.A."})]})]})]}),oe.categories&&oe.categories!=="k.A."&&l.jsxs("div",{className:"mt-6 pt-5 border-t border-slate-200 dark:border-slate-800/50",children:[l.jsxs("div",{className:"flex items-start gap-2 text-xs text-slate-500 mb-2",children:[l.jsx(si,{className:"h-3 w-3 mt-0.5"})," Categories"]}),l.jsx("div",{className:"flex flex-wrap gap-1.5",children:oe.categories.split(",").map(T=>l.jsx("span",{className:"px-2 py-0.5 bg-slate-100 dark:bg-slate-900 text-slate-600 dark:text-slate-400 rounded-full text-[10px] border border-slate-200 dark:border-slate-800",children:T.trim()},T))})]}),l.jsx("div",{className:"mt-4 flex justify-end",children:l.jsxs("a",{href:oe.url,target:"_blank",className:"text-[10px] text-blue-600 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 flex items-center gap-1 font-bold",children:["WIKIPEDIA ",l.jsx(ei,{className:"h-2.5 w-2.5"})]})})]})}):W?null:l.jsxs("div",{className:"p-4 rounded-xl border border-dashed border-slate-200 dark:border-slate-800 text-center text-slate-500 dark:text-slate-600",children:[l.jsx(fr,{className:"h-5 w-5 mx-auto mb-2 opacity-20"}),l.jsx("p",{className:"text-xs",children:"No Wikipedia profile found yet."})]})]}),l.jsxs("div",{children:[l.jsxs("h3",{className:"text-sm font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider mb-3 flex items-center gap-2",children:[l.jsx(Pt,{className:"h-4 w-4"})," Quantitative Potential"]}),s.calculated_metric_value!=null||s.standardized_metric_value!=null?l.jsxs("div",{className:"bg-slate-50 dark:bg-slate-950 rounded-lg p-4 border border-slate-200 dark:border-slate-800 space-y-4",children:[s.calculated_metric_value!=null&&l.jsxs("div",{className:"flex items-start gap-3",children:[l.jsx("div",{className:"p-2 bg-white dark:bg-slate-800 rounded-lg text-blue-500 mt-1",children:l.jsx(ux,{className:"h-4 w-4"})}),l.jsxs("div",{children:[l.jsx("div",{className:"text-[10px] text-slate-500 uppercase font-bold tracking-tight",children:s.calculated_metric_name||"Calculated Metric"}),l.jsxs("div",{className:"text-xl text-slate-900 dark:text-white font-bold",children:[s.calculated_metric_value.toLocaleString("de-DE"),l.jsx("span",{className:"text-sm font-medium text-slate-500 ml-1",children:s.calculated_metric_unit})]})]})]}),s.standardized_metric_value!=null&&l.jsxs("div",{className:"flex items-start gap-3 pt-4 border-t border-slate-200 dark:border-slate-800",children:[l.jsx("div",{className:"p-2 bg-white dark:bg-slate-800 rounded-lg text-green-500 mt-1",children:l.jsx(yx,{className:"h-4 w-4"})}),l.jsxs("div",{children:[l.jsxs("div",{className:"text-[10px] text-slate-500 uppercase font-bold tracking-tight",children:["Standardized Potential (",s.standardized_metric_unit,")"]}),l.jsxs("div",{className:"text-xl text-green-600 dark:text-green-400 font-bold",children:[s.standardized_metric_value.toLocaleString("de-DE"),l.jsx("span",{className:"text-sm font-medium text-slate-500 ml-1",children:s.standardized_metric_unit})]}),l.jsx("p",{className:"text-xs text-slate-500 mt-1",children:"Comparable value for potential analysis."})]})]}),s.metric_source&&l.jsxs("div",{className:"flex justify-between items-center text-[10px] text-slate-500 pt-2 border-t border-slate-200 dark:border-slate-800",children:[s.metric_confidence!=null&&l.jsxs("div",{className:"flex items-center gap-1.5",title:s.metric_confidence_reason||"No reason provided",children:[l.jsx("span",{className:"uppercase font-bold tracking-tight text-[9px]",children:"Confidence:"}),l.jsxs("div",{className:"flex items-center gap-1",children:[l.jsx("div",{className:B("h-2 w-2 rounded-full",s.metric_confidence>=.8?"bg-green-500":s.metric_confidence>=.5?"bg-yellow-500":"bg-red-500")}),l.jsxs("span",{className:B("font-medium",s.metric_confidence>=.8?"text-green-700 dark:text-green-400":s.metric_confidence>=.5?"text-yellow-700 dark:text-yellow-400":"text-red-700 dark:text-red-400"),children:[(s.metric_confidence*100).toFixed(0),"%"]})]})]}),l.jsxs("div",{className:"flex items-center gap-1",children:[l.jsx(dx,{className:"h-3 w-3"}),l.jsx("span",{children:"Source:"}),l.jsx("span",{title:s.metric_proof_text||"No proof text available",className:"font-medium text-slate-600 dark:text-slate-400 capitalize cursor-help border-b border-dotted border-slate-400",children:s.metric_source}),s.metric_source_url&&l.jsx("a",{href:s.metric_source_url,target:"_blank",rel:"noopener noreferrer",className:"ml-1 text-blue-600 dark:text-blue-400 hover:underline",children:l.jsx(ei,{className:"h-3 w-3 inline"})})]})]})]}):l.jsxs("div",{className:"p-4 rounded-xl border border-dashed border-slate-200 dark:border-slate-800 text-center text-slate-500 dark:text-slate-600",children:[l.jsx(Pt,{className:"h-5 w-5 mx-auto mb-2 opacity-20"}),l.jsx("p",{className:"text-xs",children:"No quantitative data calculated yet."}),l.jsx("p",{className:"text-xs mt-1",children:'Run "Analyze Potential" to extract metrics.'})]})]})]}),f==="contacts"&&l.jsx(Ex,{contacts:s.contacts,initialContactId:t,onAddContact:Kf,onEditContact:Jf})]})]}):l.jsx("div",{className:"p-8 text-red-400",children:"Failed to load data."})})}function Rx({apiBase:e}){const[t,n]=E.useState([]),[r,s]=E.useState([]),[a,i]=E.useState([]),[o,u]=E.useState(!1),[c,f]=E.useState("all"),[m,x]=E.useState("all"),[y,g]=E.useState(""),[k,S]=E.useState(null),[p,d]=E.useState({subject:"",intro:"",social_proof:""}),h=async()=>{try{const[w,z]=await Promise.all([D.get(`${e}/industries`),D.get(`${e}/matrix/personas`)]);s(w.data),i(z.data)}catch(w){console.error("Failed to fetch metadata:",w)}},N=async()=>{u(!0);try{const w={};c!=="all"&&(w.industry_id=c),m!=="all"&&(w.persona_id=m);const z=await D.get(`${e}/matrix`,{params:w});n(z.data)}catch(w){console.error("Failed to fetch matrix entries:",w)}finally{u(!1)}};E.useEffect(()=>{h()},[]),E.useEffect(()=>{N()},[c,m]);const C=E.useMemo(()=>{if(!y)return t;const w=y.toLowerCase();return t.filter(z=>{var W,G,Y;return z.industry_name.toLowerCase().includes(w)||z.persona_name.toLowerCase().includes(w)||((W=z.subject)==null?void 0:W.toLowerCase().includes(w))||((G=z.intro)==null?void 0:G.toLowerCase().includes(w))||((Y=z.social_proof)==null?void 0:Y.toLowerCase().includes(w))})},[t,y]),L=w=>{S(w.id),d({subject:w.subject||"",intro:w.intro||"",social_proof:w.social_proof||""})},P=()=>{S(null)},b=async w=>{try{await D.put(`${e}/matrix/${w}`,p),n(z=>z.map(W=>W.id===w?{...W,...p}:W)),S(null)}catch(z){alert("Save failed"),console.error(z)}},U=()=>{let w=`${e}/matrix/export`;const z=new URLSearchParams;c!=="all"&&z.append("industry_id",c.toString()),m!=="all"&&z.append("persona_id",m.toString()),z.toString()&&(w+=`?${z.toString()}`),window.open(w,"_blank")};return l.jsxs("div",{className:"space-y-4",children:[l.jsxs("div",{className:"flex flex-wrap items-center gap-3 bg-slate-50 dark:bg-slate-950 p-3 rounded-lg border border-slate-200 dark:border-slate-800",children:[l.jsxs("div",{className:"flex items-center gap-2",children:[l.jsx(px,{className:"h-4 w-4 text-slate-400"}),l.jsx("span",{className:"text-xs font-bold text-slate-500 uppercase",children:"Filters:"})]}),l.jsxs("select",{className:"bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 rounded px-2 py-1.5 text-xs outline-none focus:ring-1 focus:ring-blue-500",value:c,onChange:w=>f(w.target.value==="all"?"all":parseInt(w.target.value)),children:[l.jsx("option",{value:"all",children:"All Industries"}),r.map(w=>l.jsx("option",{value:w.id,children:w.name},w.id))]}),l.jsxs("select",{className:"bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 rounded px-2 py-1.5 text-xs outline-none focus:ring-1 focus:ring-blue-500",value:m,onChange:w=>x(w.target.value==="all"?"all":parseInt(w.target.value)),children:[l.jsx("option",{value:"all",children:"All Personas"}),a.map(w=>l.jsx("option",{value:w.id,children:w.name},w.id))]}),l.jsxs("div",{className:"flex-1 min-w-[200px] relative",children:[l.jsx(An,{className:"absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-slate-400"}),l.jsx("input",{type:"text",placeholder:"Search in texts...",className:"w-full bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 rounded px-9 py-1.5 text-xs outline-none focus:ring-1 focus:ring-blue-500",value:y,onChange:w=>g(w.target.value)})]}),l.jsxs("button",{onClick:U,className:"flex items-center gap-2 bg-slate-200 dark:bg-slate-800 hover:bg-slate-300 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 px-3 py-1.5 rounded text-xs font-bold transition-all border border-slate-300 dark:border-slate-700",children:[l.jsx(_f,{className:"h-3.5 w-3.5"}),"EXPORT CSV"]})]}),l.jsx("div",{className:"border border-slate-200 dark:border-slate-800 rounded-lg overflow-hidden bg-white dark:bg-slate-950",children:l.jsxs("table",{className:"w-full text-left text-xs table-fixed",children:[l.jsx("thead",{className:"bg-slate-50 dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 text-slate-500 font-bold uppercase",children:l.jsxs("tr",{children:[l.jsx("th",{className:"p-3 w-40",children:"Combination"}),l.jsx("th",{className:"p-3 w-1/4",children:"Subject Line"}),l.jsx("th",{className:"p-3 w-1/3",children:"Intro Text"}),l.jsx("th",{className:"p-3",children:"Social Proof"}),l.jsx("th",{className:"p-3 w-20 text-center",children:"Action"})]})}),l.jsx("tbody",{className:"divide-y divide-slate-100 dark:divide-slate-800/50",children:o?l.jsx("tr",{children:l.jsx("td",{colSpan:5,className:"p-12 text-center text-slate-400 italic",children:"Loading matrix entries..."})}):C.length===0?l.jsx("tr",{children:l.jsx("td",{colSpan:5,className:"p-12 text-center text-slate-400 italic",children:"No entries found for the selected filters."})}):C.map(w=>l.jsxs("tr",{className:B("group transition-colors",k===w.id?"bg-blue-50/50 dark:bg-blue-900/10":"hover:bg-slate-50/50 dark:hover:bg-slate-900/30"),children:[l.jsxs("td",{className:"p-3 align-top",children:[l.jsx("div",{className:"font-bold text-slate-900 dark:text-white leading-tight mb-1",children:w.industry_name}),l.jsx("div",{className:"text-[10px] text-blue-600 dark:text-blue-400 font-bold uppercase tracking-wider",children:w.persona_name})]}),l.jsx("td",{className:"p-3 align-top",children:k===w.id?l.jsx("input",{className:"w-full bg-white dark:bg-slate-900 border border-blue-300 dark:border-blue-700 rounded p-1.5 outline-none",value:p.subject,onChange:z=>d(W=>({...W,subject:z.target.value}))}):l.jsx("div",{className:"text-slate-700 dark:text-slate-300",children:w.subject||l.jsx("span",{className:"text-slate-400 italic",children:"Empty"})})}),l.jsx("td",{className:"p-3 align-top",children:k===w.id?l.jsx("textarea",{className:"w-full bg-white dark:bg-slate-900 border border-blue-300 dark:border-blue-700 rounded p-1.5 outline-none h-24 text-[11px]",value:p.intro,onChange:z=>d(W=>({...W,intro:z.target.value}))}):l.jsx("div",{className:"text-slate-600 dark:text-slate-400 line-clamp-4 hover:line-clamp-none transition-all",children:w.intro||l.jsx("span",{className:"text-slate-400 italic",children:"Empty"})})}),l.jsx("td",{className:"p-3 align-top",children:k===w.id?l.jsx("textarea",{className:"w-full bg-white dark:bg-slate-900 border border-blue-300 dark:border-blue-700 rounded p-1.5 outline-none h-24 text-[11px]",value:p.social_proof,onChange:z=>d(W=>({...W,social_proof:z.target.value}))}):l.jsx("div",{className:"text-slate-600 dark:text-slate-400 line-clamp-4 hover:line-clamp-none transition-all",children:w.social_proof||l.jsx("span",{className:"text-slate-400 italic",children:"Empty"})})}),l.jsx("td",{className:"p-3 align-top text-center",children:k===w.id?l.jsxs("div",{className:"flex flex-col gap-2",children:[l.jsx("button",{onClick:()=>b(w.id),className:"p-1.5 bg-green-600 text-white rounded hover:bg-green-500 transition-colors shadow-sm",title:"Save Changes",children:l.jsx(Pn,{className:"h-4 w-4"})}),l.jsx("button",{onClick:P,className:"p-1.5 bg-slate-200 dark:bg-slate-800 text-slate-600 dark:text-slate-400 rounded hover:bg-slate-300 dark:hover:bg-slate-700 transition-colors",title:"Cancel",children:l.jsx(rt,{className:"h-4 w-4"})})]}):l.jsx("button",{onClick:()=>L(w),className:"p-2 text-slate-400 hover:text-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-full transition-all opacity-0 group-hover:opacity-100",title:"Edit Entry",children:l.jsx(gx,{className:"h-4 w-4"})})})]},w.id))})]})})]})}function Px({isOpen:e,onClose:t,apiBase:n}){const[r,s]=E.useState(localStorage.getItem("roboticsSettingsActiveTab")||"robotics"),[a,i]=E.useState([]),[o,u]=E.useState([]),[c,f]=E.useState([]),[m,x]=E.useState([]),[y,g]=E.useState([]),[k,S]=E.useState("PENDING"),[p,d]=E.useState(!1),[h,N]=E.useState(!1),[C,L]=E.useState(""),P=E.useMemo(()=>{const j=c.reduce((V,_)=>{const M=_.role||"Unassigned";return V[M]||(V[M]=[]),V[M].push(_),V},{});if(!C)return j;const I={};for(const V in j){const _=j[V].filter(M=>M.pattern_value.toLowerCase().includes(C.toLowerCase()));_.length>0&&(I[V]=_)}return I},[c,C]),b=async()=>{d(!0);try{const[j,I,V,_,M]=await Promise.all([D.get(`${n}/robotics/categories`),D.get(`${n}/industries`),D.get(`${n}/job_roles`),D.get(`${n}/job_roles/raw?unmapped_only=true`),D.get(`${n}/mistakes?status=${k}`)]);i(j.data),u(I.data),f(V.data),x(_.data),g(M.data.items)}catch(j){console.error("Failed to fetch settings data:",j),alert("Fehler beim Laden der Settings. Siehe Konsole.")}finally{d(!1)}};E.useEffect(()=>{e&&b()},[e]),E.useEffect(()=>{e&&(async()=>{d(!0);try{const I=await D.get(`${n}/mistakes?status=${k}`);g(I.data.items)}catch(I){console.error(I)}finally{d(!1)}})()},[k]),E.useEffect(()=>{localStorage.setItem("roboticsSettingsActiveTab",r)},[r]);const U=async(j,I,V)=>{d(!0);try{await D.put(`${n}/robotics/categories/${j}`,{description:I,reasoning_guide:V}),b()}catch(_){alert("Update failed"),console.error(_)}finally{d(!1)}},w=async(j,I)=>{d(!0);try{await D.put(`${n}/mistakes/${j}`,{status:I}),b()}catch(V){alert("Failed to update mistake status"),console.error(V)}finally{d(!1)}},z=async()=>{if(window.confirm(`This will send all ${m.length} unmapped job titles to the AI for classification. This may take a few minutes. Continue?`)){N(!0);try{await D.post(`${n}/job_roles/classify-batch`),alert("Batch classification started in the background. The list will update automatically as titles are processed. You can close this window.")}catch(j){alert("Failed to start batch classification."),console.error(j)}finally{N(!1)}}},W=async(j,I,V)=>{const _=c.find(A=>A.id===j);if(!_)return;const M={..._,[I]:V};I==="priority"&&(M.priority=parseInt(V,10));try{await D.put(`${n}/job_roles/${j}`,M),f(c.map(A=>A.id===j?M:A))}catch(A){alert("Failed to update job role"),console.error(A)}},G=async j=>{const I=j||"New Pattern";d(!0);try{await D.post(`${n}/job_roles`,{pattern_type:"exact",pattern_value:I,role:"Influencer",priority:100}),b()}catch(V){alert("Failed to add job role"),console.error(V)}finally{d(!1)}},Y=async j=>{if(window.confirm("Are you sure you want to delete this pattern?")){d(!0);try{await D.delete(`${n}/job_roles/${j}`),b()}catch(I){alert("Failed to delete job role"),console.error(I)}finally{d(!1)}}};return e?l.jsx("div",{className:"fixed inset-0 z-[60] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200",children:l.jsxs("div",{className:"bg-white dark:bg-slate-900 w-full max-w-4xl max-h-[85vh] rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-800 flex flex-col overflow-hidden",children:[l.jsxs("div",{className:"p-6 border-b border-slate-200 dark:border-slate-800 flex justify-between items-center bg-slate-50 dark:bg-slate-950/50",children:[l.jsxs("div",{children:[l.jsx("h2",{className:"text-xl font-bold text-slate-900 dark:text-white",children:"Settings & Classification Logic"}),l.jsx("p",{className:"text-sm text-slate-500",children:"Define how AI evaluates leads and matches roles."})]}),l.jsx("button",{onClick:t,className:"p-2 hover:bg-slate-200 dark:hover:bg-slate-800 rounded-full transition-colors text-slate-500",children:l.jsx(rt,{className:"h-6 w-6"})})]}),l.jsx("div",{className:"flex flex-shrink-0 border-b border-slate-200 dark:border-slate-800 px-6 bg-white dark:bg-slate-900 overflow-x-auto",children:[{id:"robotics",label:"Robotics Potential",icon:Pt},{id:"industries",label:"Industry Focus",icon:Of},{id:"roles",label:"Job Role Mapping",icon:Wt},{id:"matrix",label:"Marketing Matrix",icon:hx},{id:"mistakes",label:"Reported Mistakes",icon:el}].map(j=>l.jsxs("button",{onClick:()=>s(j.id),className:B("flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-all whitespace-nowrap",r===j.id?"border-blue-500 text-blue-600 dark:text-blue-400":"border-transparent text-slate-500 hover:text-slate-800 dark:hover:text-slate-300"),children:[l.jsx(j.icon,{className:"h-4 w-4"})," ",j.label]},j.id))}),l.jsxs("div",{className:"flex-1 overflow-y-auto p-6 space-y-6 bg-white dark:bg-slate-900",children:[p&&l.jsx("div",{className:"text-center py-12 text-slate-500",children:"Loading..."}),l.jsx("div",{className:B("grid grid-cols-1 md:grid-cols-2 gap-6",{hidden:p||r!=="robotics"}),children:a.map(j=>l.jsx(Tx,{category:j,onSave:U},j.id))},"robotics-content"),l.jsxs("div",{className:B("space-y-4",{hidden:p||r!=="industries"}),children:[l.jsx("div",{className:"flex justify-between items-center",children:l.jsx("h3",{className:"text-sm font-bold text-slate-700 dark:text-slate-300",children:"Industry Verticals (Synced from Notion)"})}),l.jsx("div",{className:"grid grid-cols-1 gap-3",children:o.map(j=>{var I,V;return l.jsxs("div",{className:"bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-lg p-4 flex flex-col gap-3 group relative overflow-hidden",children:[j.notion_id&&l.jsx("div",{className:"absolute top-0 right-0 bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 text-[9px] font-bold px-2 py-0.5 rounded-bl",children:"SYNCED"}),l.jsxs("div",{className:"flex gap-4 items-start pr-12",children:[l.jsx("div",{className:"flex-1",children:l.jsxs("div",{className:"flex items-center gap-2 mb-1",children:[l.jsx("h4",{className:"font-bold text-slate-900 dark:text-white text-sm",children:j.name}),j.priority&&l.jsx("span",{className:B("text-[9px] font-bold px-1.5 py-0.5 rounded uppercase tracking-wider",j.priority==="Freigegeben"?"bg-green-100 text-green-700":"bg-purple-100 text-purple-700"),children:j.priority}),j.ops_focus_secondary&&l.jsx("span",{className:"text-[9px] font-bold px-1.5 py-0.5 rounded uppercase tracking-wider bg-orange-100 text-orange-700 border border-orange-200",children:"SEC-PRODUCT"})]})}),l.jsx("div",{className:"text-right",children:l.jsxs("div",{className:"flex items-center gap-1.5 justify-end",children:[l.jsx("span",{className:B("w-2 h-2 rounded-full",j.is_focus?"bg-green-500":"bg-slate-300 dark:bg-slate-700")}),l.jsx("span",{className:"text-xs text-slate-500",children:j.is_focus?"Focus":"Standard"})]})})]}),l.jsxs("div",{className:"space-y-2",children:[l.jsx("p",{className:"text-xs text-slate-600 dark:text-slate-300 italic whitespace-pre-wrap",children:j.description||"No definition"}),(j.pains||j.gains)&&l.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-3 mt-2",children:[j.pains&&l.jsxs("div",{className:"p-2 bg-red-50/50 dark:bg-red-900/10 rounded border border-red-100 dark:border-red-900/30",children:[l.jsx("div",{className:"text-[9px] font-bold text-red-600 dark:text-red-400 uppercase mb-1",children:"Pains"}),l.jsx("div",{className:"text-[10px] text-slate-600 dark:text-slate-400 line-clamp-3 hover:line-clamp-none transition-all",children:j.pains})]}),j.gains&&l.jsxs("div",{className:"p-2 bg-green-50/50 dark:bg-green-900/10 rounded border border-green-100 dark:border-green-900/30",children:[l.jsx("div",{className:"text-[9px] font-bold text-green-600 dark:text-green-400 uppercase mb-1",children:"Gains"}),l.jsx("div",{className:"text-[10px] text-slate-600 dark:text-slate-400 line-clamp-3 hover:line-clamp-none transition-all",children:j.gains})]})]}),j.notes&&l.jsxs("div",{className:"text-[10px] text-slate-500 border-l-2 border-slate-200 dark:border-slate-800 pl-2 py-1",children:[l.jsx("span",{className:"font-bold uppercase mr-1",children:"Notes:"})," ",j.notes]})]}),l.jsxs("div",{className:"grid grid-cols-2 sm:grid-cols-4 gap-2 text-[10px] bg-white dark:bg-slate-900 p-2 rounded border border-slate-200 dark:border-slate-800",children:[l.jsxs("div",{children:[l.jsx("span",{className:"block text-slate-400 font-bold uppercase",children:"Whale >"}),l.jsx("span",{className:"text-slate-700 dark:text-slate-200",children:j.whale_threshold||"-"})]}),l.jsxs("div",{children:[l.jsx("span",{className:"block text-slate-400 font-bold uppercase",children:"Min Req"}),l.jsx("span",{className:"text-slate-700 dark:text-slate-200",children:j.min_requirement||"-"})]}),l.jsxs("div",{children:[l.jsx("span",{className:"block text-slate-400 font-bold uppercase",children:"Unit"}),l.jsx("span",{className:"text-slate-700 dark:text-slate-200 truncate",children:j.scraper_search_term||"-"})]}),l.jsxs("div",{children:[l.jsx("span",{className:"block text-slate-400 font-bold uppercase",children:"Product"}),l.jsx("span",{className:"text-slate-700 dark:text-slate-200 truncate",children:((I=a.find(_=>_.id===j.primary_category_id))==null?void 0:I.name)||"-"}),j.secondary_category_id&&l.jsxs("div",{className:"mt-1 pt-1 border-t border-slate-100 dark:border-slate-800",children:[l.jsx("span",{className:"block text-orange-400 font-bold uppercase text-[9px]",children:"Sec. Prod"}),l.jsx("span",{className:"text-slate-700 dark:text-slate-200 truncate",children:((V=a.find(_=>_.id===j.secondary_category_id))==null?void 0:V.name)||"-"})]})]})]}),j.scraper_keywords&&l.jsxs("div",{className:"text-[10px]",children:[l.jsx("span",{className:"text-slate-400 font-bold uppercase mr-2",children:"Keywords:"}),l.jsx("span",{className:"text-slate-600 dark:text-slate-400 font-mono",children:j.scraper_keywords})]}),j.standardization_logic&&l.jsxs("div",{className:"text-[10px]",children:[l.jsx("span",{className:"text-slate-400 font-bold uppercase mr-2",children:"Standardization:"}),l.jsx("span",{className:"text-slate-600 dark:text-slate-400 font-mono",children:j.standardization_logic})]})]},j.id)})})]},"industries-content"),l.jsxs("div",{className:B("space-y-8",{hidden:p||r!=="roles"}),children:[l.jsxs("div",{className:"space-y-4",children:[l.jsxs("div",{className:"flex justify-between items-center gap-4",children:[l.jsx("div",{className:"flex-1",children:l.jsx("input",{type:"text",placeholder:"Search patterns...",value:C,onChange:j=>L(j.target.value),className:"w-full bg-white dark:bg-slate-950 border border-slate-300 dark:border-slate-700 rounded-md px-3 py-1.5 text-xs text-slate-900 dark:text-white focus:ring-1 focus:ring-blue-500 outline-none"})}),l.jsxs("button",{onClick:()=>G(),className:"flex items-center gap-1 px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white text-xs font-bold rounded shadow-lg shadow-blue-500/20",children:[l.jsx(ni,{className:"h-3 w-3"})," ADD PATTERN"]})]}),l.jsx("div",{className:"space-y-2",children:Object.keys(P).sort().map(j=>l.jsxs("details",{className:"bg-white dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-lg group",open:!!C,children:[l.jsxs("summary",{className:"p-3 cursor-pointer flex justify-between items-center group-hover:bg-slate-50 dark:group-hover:bg-slate-900 transition-colors",children:[l.jsxs("div",{className:"font-semibold text-slate-800 dark:text-slate-200 text-xs",children:[j,l.jsxs("span",{className:"ml-2 text-slate-400 font-normal",children:["(",P[j].length," patterns)"]})]}),l.jsx(cx,{className:"h-4 w-4 text-slate-400 transform group-open:rotate-180 transition-transform"})]}),l.jsx("div",{className:"border-t border-slate-200 dark:border-slate-800",children:l.jsxs("table",{className:"w-full text-left text-xs",children:[l.jsx("thead",{className:"bg-slate-50 dark:bg-slate-900/50 text-slate-500 font-bold uppercase tracking-wider",children:l.jsxs("tr",{children:[l.jsx("th",{className:"p-2",children:"Type"}),l.jsx("th",{className:"p-2",children:"Pattern Value"}),l.jsx("th",{className:"p-2",children:"Priority"}),l.jsx("th",{className:"p-2 w-8"})]})}),l.jsx("tbody",{className:"divide-y divide-slate-100 dark:divide-slate-800/50",children:P[j].map(I=>l.jsxs("tr",{className:"group/row hover:bg-slate-50/50 dark:hover:bg-slate-800/30 transition-colors",children:[l.jsx("td",{className:"p-1.5",children:l.jsxs("select",{className:"w-full bg-transparent border border-transparent hover:border-slate-300 dark:hover:border-slate-700 rounded px-1 py-0.5 text-slate-900 dark:text-slate-200 outline-none focus:border-blue-500",defaultValue:I.pattern_type,onChange:V=>W(I.id,"pattern_type",V.target.value),children:[l.jsx("option",{children:"exact"}),l.jsx("option",{children:"regex"})]})}),l.jsx("td",{className:"p-1.5",children:l.jsx("input",{className:"w-full bg-transparent border border-transparent hover:border-slate-300 dark:hover:border-slate-700 rounded px-1 py-0.5 text-slate-900 dark:text-slate-200 outline-none focus:border-blue-500 font-mono",defaultValue:I.pattern_value,onBlur:V=>W(I.id,"pattern_value",V.target.value)})}),l.jsx("td",{className:"p-1.5",children:l.jsx("input",{type:"number",className:"w-16 bg-transparent border border-transparent hover:border-slate-300 dark:hover:border-slate-700 rounded px-1 py-0.5 text-slate-900 dark:text-slate-200 outline-none focus:border-blue-500 font-mono",defaultValue:I.priority,onBlur:V=>W(I.id,"priority",V.target.value)})}),l.jsx("td",{className:"p-1.5 text-center",children:l.jsx("button",{onClick:()=>Y(I.id),className:"text-slate-400 hover:text-red-500 opacity-0 group-hover/row:opacity-100 transition-all transform hover:scale-110",children:l.jsx(Mf,{className:"h-4 w-4"})})})]},I.id))})]})})]},j))})]}),l.jsxs("div",{className:"space-y-4 pt-4 border-t border-slate-200 dark:border-slate-800",children:[l.jsxs("div",{className:"flex justify-between items-center",children:[l.jsxs("div",{children:[l.jsx("h3",{className:"text-sm font-bold text-slate-700 dark:text-slate-300",children:"Discovery Inbox"}),l.jsx("p",{className:"text-[10px] text-slate-500 uppercase font-semibold",children:"Unmapped job titles from CRM, prioritized by frequency"})]}),m.length>0&&l.jsxs("button",{onClick:z,disabled:h,className:"flex items-center gap-1 px-3 py-1.5 bg-green-600 hover:bg-green-500 text-white text-xs font-bold rounded shadow-lg shadow-green-500/20 disabled:bg-slate-400 disabled:shadow-none",children:[l.jsx(Pt,{className:"h-3 w-3"}),h?"CLASSIFYING...":`CLASSIFY ${m.length} TITLES`]})]}),l.jsx("div",{className:"bg-slate-50/50 dark:bg-slate-900/20 border border-dashed border-slate-300 dark:border-slate-700 rounded-xl overflow-hidden",children:l.jsxs("table",{className:"w-full text-left text-xs",children:[l.jsx("thead",{className:"bg-slate-100/50 dark:bg-slate-900/80 border-b border-slate-200 dark:border-slate-800 text-slate-400 font-bold uppercase tracking-wider",children:l.jsxs("tr",{children:[l.jsx("th",{className:"p-3",children:"Job Title from CRM"}),l.jsx("th",{className:"p-3 w-20 text-center",children:"Frequency"}),l.jsx("th",{className:"p-3 w-10"})]})}),l.jsxs("tbody",{className:"divide-y divide-slate-100 dark:divide-slate-800/50",children:[m.map(j=>l.jsxs("tr",{className:"group hover:bg-white dark:hover:bg-slate-800 transition-colors",children:[l.jsx("td",{className:"p-3 font-medium text-slate-600 dark:text-slate-400 italic",children:j.title}),l.jsx("td",{className:"p-3 text-center",children:l.jsxs("span",{className:"px-2 py-1 bg-slate-200 dark:bg-slate-800 rounded-full font-bold text-[10px] text-slate-500",children:[j.count,"x"]})}),l.jsx("td",{className:"p-3 text-center",children:l.jsx("button",{onClick:()=>G(j.title),className:"p-1 text-blue-500 hover:bg-blue-100 dark:hover:bg-blue-900/30 rounded transition-all",children:l.jsx(ni,{className:"h-4 w-4"})})})]},j.id)),m.length===0&&l.jsx("tr",{children:l.jsx("td",{colSpan:3,className:"p-12 text-center text-slate-400 italic",children:"Discovery inbox is empty. Import raw job titles to see data here."})})]})]})})]})]},"roles-content"),l.jsx("div",{className:B("space-y-4",{hidden:p||r!=="matrix"}),children:l.jsx(Rx,{apiBase:n})},"matrix-content"),l.jsxs("div",{className:B("space-y-4",{hidden:p||r!=="mistakes"}),children:[l.jsxs("div",{className:"flex justify-between items-center",children:[l.jsx("h3",{className:"text-sm font-bold text-slate-700 dark:text-slate-300",children:"Reported Data Mistakes"}),l.jsxs("div",{className:"flex items-center gap-2",children:[l.jsx("span",{className:"text-xs text-slate-500",children:"Filter:"}),l.jsxs("select",{value:k,onChange:j=>S(j.target.value),className:"bg-slate-50 dark:bg-slate-800 border border-slate-300 dark:border-slate-700 rounded-md px-2 py-1 text-xs text-slate-900 dark:text-white focus:ring-1 focus:ring-blue-500 outline-none",children:[l.jsx("option",{value:"PENDING",children:"Pending"}),l.jsx("option",{value:"APPROVED",children:"Approved"}),l.jsx("option",{value:"REJECTED",children:"Rejected"}),l.jsx("option",{value:"ALL",children:"All"})]})]})]}),l.jsx("div",{className:"bg-slate-50 dark:bg-slate-950 border border-slate-200 dark:border-slate-800 rounded-lg overflow-hidden",children:l.jsxs("table",{className:"w-full text-left text-xs",children:[l.jsx("thead",{className:"bg-slate-100 dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 text-slate-500 font-bold uppercase",children:l.jsxs("tr",{children:[l.jsx("th",{className:"p-3",children:"Company"}),l.jsx("th",{className:"p-3",children:"Field"}),l.jsx("th",{className:"p-3",children:"Wrong Value"}),l.jsx("th",{className:"p-3",children:"Corrected Value"}),l.jsx("th",{className:"p-3",children:"Source / Quote / Comment"}),l.jsx("th",{className:"p-3",children:"Status"}),l.jsx("th",{className:"p-3 w-10",children:"Actions"})]})}),l.jsx("tbody",{className:"divide-y divide-slate-200 dark:divide-slate-800",children:y.length>0?y.map(j=>l.jsxs("tr",{className:"group",children:[l.jsx("td",{className:"p-2 font-medium text-slate-900 dark:text-slate-200",children:j.company.name}),l.jsx("td",{className:"p-2 text-slate-700 dark:text-slate-300",children:j.field_name}),l.jsx("td",{className:"p-2 text-red-600 dark:text-red-400",children:j.wrong_value||"-"}),l.jsx("td",{className:"p-2 text-green-600 dark:text-green-400",children:j.corrected_value||"-"}),l.jsxs("td",{className:"p-2 text-slate-500",children:[j.source_url&&l.jsxs("a",{href:j.source_url,target:"_blank",rel:"noopener noreferrer",className:"text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1 mb-1",children:[l.jsx(ei,{className:"h-3 w-3"})," Source"]}),j.quote&&l.jsxs("p",{className:"italic text-[10px] my-1",children:['"',j.quote,'"']}),j.user_comment&&l.jsxs("p",{className:"text-[10px]",children:["Comment: ",j.user_comment]})]}),l.jsx("td",{className:"p-2",children:l.jsx("span",{className:B("px-2 py-0.5 rounded-full text-[10px] font-semibold",{"bg-yellow-100 text-yellow-700":j.status==="PENDING","bg-green-100 text-green-700":j.status==="APPROVED","bg-red-100 text-red-700":j.status==="REJECTED"}),children:j.status})}),l.jsx("td",{className:"p-2 text-center",children:j.status==="PENDING"&&l.jsxs("div",{className:"flex gap-1 justify-center opacity-0 group-hover:opacity-100 transition-opacity",children:[l.jsx("button",{onClick:()=>w(j.id,"APPROVED"),className:"text-green-600 hover:text-green-700",title:"Approve Mistake",children:l.jsx(Pn,{className:"h-4 w-4"})}),l.jsx("button",{onClick:()=>w(j.id,"REJECTED"),className:"text-red-600 hover:text-red-700",title:"Reject Mistake",children:l.jsx(ox,{className:"h-4 w-4"})})]})})]},j.id)):l.jsx("tr",{children:l.jsx("td",{colSpan:7,className:"p-8 text-center text-slate-500 italic",children:"No reported mistakes found."})})})]})})]},"mistakes-content")]})]})}):null}function Tx({category:e,onSave:t}){const[n,r]=E.useState(e.description),[s,a]=E.useState(e.reasoning_guide),[i,o]=E.useState(!1);return E.useEffect(()=>{o(n!==e.description||s!==e.reasoning_guide)},[n,s]),l.jsxs("div",{className:"bg-slate-50 dark:bg-slate-950/50 border border-slate-200 dark:border-slate-800 rounded-xl p-4 flex flex-col gap-3",children:[l.jsxs("div",{className:"flex items-center gap-2",children:[l.jsx("div",{className:"p-1.5 bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 rounded",children:l.jsx(si,{className:"h-4 w-4"})}),l.jsx("span",{className:"font-bold text-slate-900 dark:text-white uppercase tracking-tight text-sm",children:e.name})]}),l.jsxs("div",{className:"space-y-1",children:[l.jsx("label",{className:"text-[10px] uppercase font-bold text-slate-500",children:"Definition for LLM"}),l.jsx("textarea",{className:"w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded p-2 text-xs text-slate-800 dark:text-slate-200 focus:ring-1 focus:ring-blue-500 outline-none h-20",value:n,onChange:u=>r(u.target.value)})]}),l.jsxs("div",{className:"space-y-1",children:[l.jsx("label",{className:"text-[10px] uppercase font-bold text-slate-500",children:"Reasoning Guide (Scoring)"}),l.jsx("textarea",{className:"w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded p-2 text-xs text-slate-800 dark:text-slate-200 focus:ring-1 focus:ring-blue-500 outline-none h-20",value:s,onChange:u=>a(u.target.value)})]}),i&&l.jsxs("button",{onClick:()=>t(e.id,n,s),className:"mt-2 bg-blue-600 hover:bg-blue-500 text-white text-[10px] font-bold py-1.5 rounded transition-all animate-in fade-in flex items-center justify-center gap-1",children:[l.jsx(Lf,{className:"h-3 w-3"})," SAVE CHANGES"]})]})}const gn="/ce/api";function Lx(){const[e,t]=E.useState(0),[n,r]=E.useState(!1),[s,a]=E.useState(!1),[i,o]=E.useState(null),[u,c]=E.useState(null),[f,m]=E.useState(""),[x,y]=E.useState("companies"),[g,k]=E.useState(()=>typeof window<"u"&&window.localStorage&&localStorage.getItem("theme")||"dark");E.useEffect(()=>{g==="dark"?document.documentElement.classList.add("dark"):document.documentElement.classList.remove("dark"),localStorage.setItem("theme",g)},[g]),E.useEffect(()=>{fetch(`${gn}/health`).then(h=>h.json()).then(h=>m(h.version||"")).catch(()=>m("N/A"))},[]);const S=()=>k(h=>h==="dark"?"light":"dark"),p=h=>{o(h),c(null)},d=()=>{o(null),c(null)};return l.jsxs("div",{className:"min-h-screen bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-200 font-sans transition-colors",children:[l.jsx(Cx,{isOpen:n,onClose:()=>r(!1),apiBase:gn,onSuccess:()=>t(h=>h+1)}),l.jsx(Px,{isOpen:s,onClose:()=>a(!1),apiBase:gn}),l.jsx(_x,{companyId:i,initialContactId:u,onClose:d,apiBase:gn}),l.jsxs("header",{className:"border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900/50 sticky top-0 z-10 backdrop-blur-md",children:[l.jsxs("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between",children:[l.jsxs("div",{className:"flex items-center gap-3",children:[l.jsx("div",{className:"p-2 bg-blue-600 rounded-lg",children:l.jsx(mx,{className:"h-6 w-6 text-white"})}),l.jsxs("div",{children:[l.jsx("h1",{className:"text-xl font-bold text-slate-900 dark:text-white tracking-tight",children:"Company Explorer"}),l.jsxs("p",{className:"text-xs text-blue-600 dark:text-blue-400 font-medium",children:["ROBOTICS EDITION ",f&&l.jsxs("span",{className:"text-slate-500 dark:text-slate-600 ml-2",children:["v",f]})]})]})]}),l.jsxs("div",{className:"flex items-center gap-2 md:gap-4",children:[l.jsxs("div",{className:"hidden md:flex bg-slate-100 dark:bg-slate-800 rounded-lg p-1",children:[l.jsxs("button",{onClick:()=>y("companies"),className:B("px-3 py-1.5 rounded-md text-sm font-medium transition-all flex items-center gap-2",x==="companies"?"bg-white dark:bg-slate-700 shadow text-blue-600 dark:text-white":"text-slate-500 hover:text-slate-900 dark:hover:text-slate-300"),children:[l.jsx(Ir,{className:"h-4 w-4"})," Companies"]}),l.jsxs("button",{onClick:()=>y("contacts"),className:B("px-3 py-1.5 rounded-md text-sm font-medium transition-all flex items-center gap-2",x==="contacts"?"bg-white dark:bg-slate-700 shadow text-blue-600 dark:text-white":"text-slate-500 hover:text-slate-900 dark:hover:text-slate-300"),children:[l.jsx(Wt,{className:"h-4 w-4"})," Contacts"]})]}),l.jsx("div",{className:"h-6 w-px bg-slate-300 dark:bg-slate-700 mx-2 hidden md:block"}),l.jsx("button",{onClick:S,className:"p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-full transition-colors text-slate-500 dark:text-slate-400",title:"Toggle Theme",children:g==="dark"?l.jsx(bx,{className:"h-5 w-5"}):l.jsx(xx,{className:"h-5 w-5"})}),l.jsx("button",{onClick:()=>a(!0),className:"p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-full transition-colors text-slate-500 dark:text-slate-400",title:"Configure Robotics Logic",children:l.jsx(wx,{className:"h-5 w-5"})}),l.jsx("button",{onClick:()=>t(h=>h+1),className:"p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-full transition-colors text-slate-500 dark:text-slate-400",title:"Refresh Data",children:l.jsx(ri,{className:"h-5 w-5"})}),x==="companies"&&l.jsxs("button",{className:"hidden md:flex items-center gap-2 bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded-md font-medium text-sm transition-all shadow-lg shadow-blue-900/20",onClick:()=>r(!0),children:[l.jsx(Df,{className:"h-4 w-4"}),"Import List"]})]})]}),l.jsxs("div",{className:"md:hidden border-t border-slate-200 dark:border-slate-800 flex",children:[l.jsxs("button",{onClick:()=>y("companies"),className:B("flex-1 py-3 text-sm font-medium flex justify-center items-center gap-2 border-b-2",x==="companies"?"border-blue-500 text-blue-600 dark:text-blue-400":"border-transparent text-slate-500"),children:[l.jsx(Ir,{className:"h-4 w-4"})," Companies"]}),l.jsxs("button",{onClick:()=>y("contacts"),className:B("flex-1 py-3 text-sm font-medium flex justify-center items-center gap-2 border-b-2",x==="contacts"?"border-blue-500 text-blue-600 dark:text-blue-400":"border-transparent text-slate-500"),children:[l.jsx(Wt,{className:"h-4 w-4"})," Contacts"]})]})]}),l.jsx("main",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 h-[calc(100vh-4rem)]",children:l.jsx("div",{className:"bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl overflow-hidden shadow-sm dark:shadow-xl h-full",children:x==="companies"?l.jsx(jx,{refreshKey:e,apiBase:gn,onRowClick:p,onImportClick:()=>r(!0)}):l.jsx(Sx,{apiBase:gn,onCompanyClick:h=>{o(h),y("companies")},onContactClick:(h,N)=>{o(h),c(N)}})})})]})}ra.createRoot(document.getElementById("root")).render(l.jsx(g0.StrictMode,{children:l.jsx(Lx,{})})); diff --git a/company-explorer/frontend/dist/index.html b/company-explorer/frontend/dist/index.html new file mode 100644 index 00000000..d9bc2c23 --- /dev/null +++ b/company-explorer/frontend/dist/index.html @@ -0,0 +1,13 @@ + + + + + + Company Explorer (Robotics) + + + + +
+ + diff --git a/company-explorer/frontend/package-lock.json b/company-explorer/frontend/package-lock.json deleted file mode 100644 index a1afd6cd..00000000 --- a/company-explorer/frontend/package-lock.json +++ /dev/null @@ -1,3003 +0,0 @@ -{ - "name": "company-explorer-frontend", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "company-explorer-frontend", - "version": "0.1.0", - "dependencies": { - "@tanstack/react-table": "^8.10.7", - "axios": "^1.6.2", - "clsx": "^2.0.0", - "lucide-react": "^0.294.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "tailwind-merge": "^2.1.0" - }, - "devDependencies": { - "@types/node": "^20.10.4", - "@types/react": "^18.2.43", - "@types/react-dom": "^18.2.17", - "@vitejs/plugin-react": "^4.2.1", - "autoprefixer": "^10.4.16", - "postcss": "^8.4.32", - "tailwindcss": "^3.3.6", - "typescript": "^5.3.3", - "vite": "^5.0.8" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", - "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", - "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", - "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", - "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.6" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", - "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.6", - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.3.tgz", - "integrity": "sha512-qyX8+93kK/7R5BEXPC2PjUt0+fS/VO2BVHjEHyIEWiYn88rcRBHmdLgoJjktBltgAf+NY7RfCGB1SoyKS/p9kg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.3.tgz", - "integrity": "sha512-6sHrL42bjt5dHQzJ12Q4vMKfN+kUnZ0atHHnv4V0Wd9JMTk7FDzSY35+7qbz3ypQYMBPANbpGK7JpnWNnhGt8g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.3.tgz", - "integrity": "sha512-1ht2SpGIjEl2igJ9AbNpPIKzb1B5goXOcmtD0RFxnwNuMxqkR6AUaaErZz+4o+FKmzxcSNBOLrzsICZVNYa1Rw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.3.tgz", - "integrity": "sha512-FYZ4iVunXxtT+CZqQoPVwPhH7549e/Gy7PIRRtq4t5f/vt54pX6eG9ebttRH6QSH7r/zxAFA4EZGlQ0h0FvXiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.3.tgz", - "integrity": "sha512-M/mwDCJ4wLsIgyxv2Lj7Len+UMHd4zAXu4GQ2UaCdksStglWhP61U3uowkaYBQBhVoNpwx5Hputo8eSqM7K82Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.3.tgz", - "integrity": "sha512-5jZT2c7jBCrMegKYTYTpni8mg8y3uY8gzeq2ndFOANwNuC/xJbVAoGKR9LhMDA0H3nIhvaqUoBEuJoICBudFrA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.3.tgz", - "integrity": "sha512-YeGUhkN1oA+iSPzzhEjVPS29YbViOr8s4lSsFaZKLHswgqP911xx25fPOyE9+khmN6W4VeM0aevbDp4kkEoHiA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.3.tgz", - "integrity": "sha512-eo0iOIOvcAlWB3Z3eh8pVM8hZ0oVkK3AjEM9nSrkSug2l15qHzF3TOwT0747omI6+CJJvl7drwZepT+re6Fy/w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.3.tgz", - "integrity": "sha512-DJay3ep76bKUDImmn//W5SvpjRN5LmK/ntWyeJs/dcnwiiHESd3N4uteK9FDLf0S0W8E6Y0sVRXpOCoQclQqNg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.3.tgz", - "integrity": "sha512-BKKWQkY2WgJ5MC/ayvIJTHjy0JUGb5efaHCUiG/39sSUvAYRBaO3+/EK0AZT1RF3pSj86O24GLLik9mAYu0IJg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.3.tgz", - "integrity": "sha512-Q9nVlWtKAG7ISW80OiZGxTr6rYtyDSkauHUtvkQI6TNOJjFvpj4gcH+KaJihqYInnAzEEUetPQubRwHef4exVg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.3.tgz", - "integrity": "sha512-2H5LmhzrpC4fFRNwknzmmTvvyJPHwESoJgyReXeFoYYuIDfBhP29TEXOkCJE/KxHi27mj7wDUClNq78ue3QEBQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.3.tgz", - "integrity": "sha512-9S542V0ie9LCTznPYlvaeySwBeIEa7rDBgLHKZ5S9DBgcqdJYburabm8TqiqG6mrdTzfV5uttQRHcbKff9lWtA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.3.tgz", - "integrity": "sha512-ukxw+YH3XXpcezLgbJeasgxyTbdpnNAkrIlFGDl7t+pgCxZ89/6n1a+MxlY7CegU+nDgrgdqDelPRNQ/47zs0g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.3.tgz", - "integrity": "sha512-Iauw9UsTTvlF++FhghFJjqYxyXdggXsOqGpFBylaRopVpcbfyIIsNvkf9oGwfgIcf57z3m8+/oSYTo6HutBFNw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.3.tgz", - "integrity": "sha512-3OqKAHSEQXKdq9mQ4eajqUgNIK27VZPW3I26EP8miIzuKzCJ3aW3oEn2pzF+4/Hj/Moc0YDsOtBgT5bZ56/vcA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.3.tgz", - "integrity": "sha512-0CM8dSVzVIaqMcXIFej8zZrSFLnGrAE8qlNbbHfTw1EEPnFTg1U1ekI0JdzjPyzSfUsHWtodilQQG/RA55berA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.3.tgz", - "integrity": "sha512-+fgJE12FZMIgBaKIAGd45rxf+5ftcycANJRWk8Vz0NnMTM5rADPGuRFTYar+Mqs560xuART7XsX2lSACa1iOmQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.3.tgz", - "integrity": "sha512-tMD7NnbAolWPzQlJQJjVFh/fNH3K/KnA7K8gv2dJWCwwnaK6DFCYST1QXYWfu5V0cDwarWC8Sf/cfMHniNq21A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.3.tgz", - "integrity": "sha512-u5KsqxOxjEeIbn7bUK1MPM34jrnPwjeqgyin4/N6e/KzXKfpE9Mi0nCxcQjaM9lLmPcHmn/xx1yOjgTMtu1jWQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.3.tgz", - "integrity": "sha512-vo54aXwjpTtsAnb3ca7Yxs9t2INZg7QdXN/7yaoG7nPGbOBXYXQY41Km+S1Ov26vzOAzLcAjmMdjyEqS1JkVhw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.3.tgz", - "integrity": "sha512-HI+PIVZ+m+9AgpnY3pt6rinUdRYrGHvmVdsNQ4odNqQ/eRF78DVpMR7mOq7nW06QxpczibwBmeQzB68wJ+4W4A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.3.tgz", - "integrity": "sha512-vRByotbdMo3Wdi+8oC2nVxtc3RkkFKrGaok+a62AT8lz/YBuQjaVYAS5Zcs3tPzW43Vsf9J0wehJbUY5xRSekA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.3.tgz", - "integrity": "sha512-POZHq7UeuzMJljC5NjKi8vKMFN6/5EOqcX1yGntNLp7rUTpBAXQ1hW8kWPFxYLv07QMcNM75xqVLGPWQq6TKFA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.3.tgz", - "integrity": "sha512-aPFONczE4fUFKNXszdvnd2GqKEYQdV5oEsIbKPujJmWlCI9zEsv1Otig8RKK+X9bed9gFUN6LAeN4ZcNuu4zjg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@tanstack/react-table": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", - "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", - "license": "MIT", - "dependencies": { - "@tanstack/table-core": "8.21.3" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/@tanstack/table-core": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", - "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", - "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.27", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", - "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/autoprefixer": { - "version": "10.4.23", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", - "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001760", - "fraction.js": "^5.3.4", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.16", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.16.tgz", - "integrity": "sha512-KeUZdBuxngy825i8xvzaK1Ncnkx0tBmb3k8DkEuqjKRkmtvNTjey2ZsNeh8Dw4lfKvbCOu9oeNx2TKm2vHqcRw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001765", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", - "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", - "dev": true, - "license": "ISC" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lucide-react": { - "version": "0.294.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.294.0.tgz", - "integrity": "sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.55.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.3.tgz", - "integrity": "sha512-y9yUpfQvetAjiDLtNMf1hL9NXchIJgWt6zIKeoB+tCd3npX08Eqfzg60V9DhIGVMtQ0AlMkFw5xa+AQ37zxnAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.55.3", - "@rollup/rollup-android-arm64": "4.55.3", - "@rollup/rollup-darwin-arm64": "4.55.3", - "@rollup/rollup-darwin-x64": "4.55.3", - "@rollup/rollup-freebsd-arm64": "4.55.3", - "@rollup/rollup-freebsd-x64": "4.55.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.55.3", - "@rollup/rollup-linux-arm-musleabihf": "4.55.3", - "@rollup/rollup-linux-arm64-gnu": "4.55.3", - "@rollup/rollup-linux-arm64-musl": "4.55.3", - "@rollup/rollup-linux-loong64-gnu": "4.55.3", - "@rollup/rollup-linux-loong64-musl": "4.55.3", - "@rollup/rollup-linux-ppc64-gnu": "4.55.3", - "@rollup/rollup-linux-ppc64-musl": "4.55.3", - "@rollup/rollup-linux-riscv64-gnu": "4.55.3", - "@rollup/rollup-linux-riscv64-musl": "4.55.3", - "@rollup/rollup-linux-s390x-gnu": "4.55.3", - "@rollup/rollup-linux-x64-gnu": "4.55.3", - "@rollup/rollup-linux-x64-musl": "4.55.3", - "@rollup/rollup-openbsd-x64": "4.55.3", - "@rollup/rollup-openharmony-arm64": "4.55.3", - "@rollup/rollup-win32-arm64-msvc": "4.55.3", - "@rollup/rollup-win32-ia32-msvc": "4.55.3", - "@rollup/rollup-win32-x64-gnu": "4.55.3", - "@rollup/rollup-win32-x64-msvc": "4.55.3", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwind-merge": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", - "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.19", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", - "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - } - } -} diff --git a/company-explorer/frontend/package.json b/company-explorer/frontend/package.json index 5458397a..704bd928 100644 --- a/company-explorer/frontend/package.json +++ b/company-explorer/frontend/package.json @@ -22,6 +22,9 @@ "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.4.0", "typescript": "^5.3.3", "vite": "^5.0.10" } diff --git a/company-explorer/frontend/src/App.tsx b/company-explorer/frontend/src/App.tsx index ef901cea..d2e694a2 100644 --- a/company-explorer/frontend/src/App.tsx +++ b/company-explorer/frontend/src/App.tsx @@ -4,7 +4,7 @@ import { ContactsTable } from './components/ContactsTable' // NEW import { ImportWizard } from './components/ImportWizard' import { Inspector } from './components/Inspector' import { RoboticsSettings } from './components/RoboticsSettings' -import { LayoutDashboard, UploadCloud, RefreshCw, Settings, Users, Building, Sun, Moon } from 'lucide-react' +import { LayoutDashboard, UploadCloud, RefreshCw, Settings, Users, Building, Sun, Moon, Activity } from 'lucide-react' import clsx from 'clsx' // Base URL detection (Production vs Dev) @@ -119,6 +119,16 @@ function App() { {theme === 'dark' ? : } + + + + + + + {/* Table */} +
+ + + + + + + + + + + + {isLoading ? ( + + ) : filteredEntries.length === 0 ? ( + + ) : filteredEntries.map(entry => ( + + + + + +
CombinationSubject LineIntro TextSocial ProofAction
Loading matrix entries...
No entries found for the selected filters.
+
{entry.industry_name}
+
{entry.persona_name}
+
+ {editingId === entry.id ? ( + setEditValues(v => ({ ...v, subject: e.target.value }))} + /> + ) : ( +
{entry.subject || Empty}
+ )} +
+ {editingId === entry.id ? ( +