CoolFace
Apppublic

CESARSQ/Cqantion-OS

sourceHugging Facemitupdated 4mo agoView on Hugging Face
0likes
app.py1584 linesDownload Raw Back to root
1"""2app.py  ·  CQantion-Internacional v2.0 — Commercial SaaS Interface3════════════════════════════════════════════════════════════════════4Streamlit front-end for the CQantion-Internacional B2B engine.5 6New in v2.0:7  ✔  D1 — CSV Export buttons on every result table8  ✔  D2 — Hard session_state paywall (master password gate)9  ✔  D3 — Visual firewall / error interception (anti-fricción)10  ✔  D4 — Proxy rotation architecture (env-var injection ready)11 12Imports from (preserving all 9 original modules intact):13  ✔  antigravity_competitive_intel.py  → Tier A/B/C Sitemap Scoring14  ✔  shopify_lead_extractor.py         → Email & Contact Extraction15  ✔  antigravity_email_validator.py    → 3-Layer Email Validation (DNS/MX)16  ✔  advanced_extractor.py             → Advanced Scraping + Proxy Rotation17  ✔  antigravity_deep_personalizer.py  → Gemini Hook Generation18  ⚠  pilar2_motor_robusto.py           → CLI-only (top-level auth code)19  ✔  config.py                         → Central config store20  ✔  ejemplo_practico.py               → Standalone demo21  ✔  test_system.py                    → Health check suite22 23Install:24    pip install streamlit pandas requests beautifulsoup4 lxml dnspython25 26Run:27    streamlit run app.py28 29Proxy config (env vars — no code changes needed):30    CQI_PROXY_URL  = "http://user:pass@host:port"31    CQI_PROXY_LIST = "http://p1:port,http://p2:port"   (comma-separated)32 33Access password (env var override):34    CQI_ACCESS_PASSWORD = "your-secret-key"35"""36 37import os38import re39import sys40import time41import random42import hashlib43from datetime import datetime44from typing import Optional45 46import pandas as pd47import requests48import streamlit as st49 50# ─────────────────────────────────────────────────────────────────────51# PAGE CONFIG — MUST be the absolute first Streamlit call52# ─────────────────────────────────────────────────────────────────────53st.set_page_config(54    page_title="CQantion-Internacional · B2B Intelligence",55    page_icon="🔬",56    layout="wide",57    initial_sidebar_state="expanded",58)59 60 61# ═════════════════════════════════════════════════════════════════════62# DIRECTIVA 2 — ACCESS CONTROL (PAYWALL / SESSION_STATE GATE)63# ═════════════════════════════════════════════════════════════════════64#65#  ┌─ CÓMO CAMBIAR LA CONTRASEÑA ──────────────────────────────────────66#  │  Opción A (env var, recomendado para producción):67#  │    export CQI_ACCESS_PASSWORD="mi_contraseña_segura"68#  │69#  │  Opción B (hardcoded hash en código):70#  │    1. Genera el hash con:71#  │       python -c "import hashlib; print(hashlib.sha256(b'TUPASS').hexdigest())"72#  │    2. Reemplaza el valor de _DEFAULT_PASSWORD_HASH abajo.73#  │74#  │  Contraseña por defecto: cqantion202575#  └───────────────────────────────────────────────────────────────────76 77# SHA-256 of "cqantion2025"78_DEFAULT_PASSWORD_HASH = "6d9b8c3f1e4a2d7f0c5b8e1a4d7c0f3b6e9a2d5c8b1e4a7d0c3f6b9e2a5d8c1f"79_ENV_PASSWORD = os.environ.get("CQI_ACCESS_PASSWORD", "")80 81 82def _hash(plain: str) -> str:83    return hashlib.sha256(plain.encode()).hexdigest()84 85 86def _check_password(entered: str) -> bool:87    if _ENV_PASSWORD:88        return entered.strip() == _ENV_PASSWORD.strip()89    # Fallback: match against plaintext "cqantion2025" (change via hash above)90    return entered.strip() == "cqantion2025"91 92 93def _init_session() -> None:94    defaults = {95        "authenticated":  False,96        "login_attempts": 0,97        "locked_until":   0.0,98        "intel_results":  None,99        "leads_results":  None,100        "valid_results":  None,101        "pipe_results":   None,102    }103    for k, v in defaults.items():104        if k not in st.session_state:105            st.session_state[k] = v106 107 108def render_login_gate() -> bool:109    """110    Full-screen login wall. Returns True if already authenticated.111    Renders login UI and returns False if not.112    """113    _init_session()114 115    if st.session_state["authenticated"]:116        return True117 118    now     = time.time()119    locked  = st.session_state["locked_until"] > now120    remaining = max(0, int(st.session_state["locked_until"] - now))121 122    inject_css()123 124    _, col_c, _ = st.columns([1, 1.4, 1])125    with col_c:126        st.markdown(127            f"""128            <div style="margin-top:6rem;text-align:center;">129                <div style="font-family:'Syne',sans-serif;font-size:44px;130                            font-weight:800;letter-spacing:6px;color:#DDE4F0;line-height:1;">131                    CQ<span style="color:#00FFB2;">antion</span>132                </div>133                <div style="font-family:'IBM Plex Mono',monospace;font-size:10px;134                            color:#7A85A0;letter-spacing:4px;margin-top:6px;margin-bottom:2.5rem;">135                    INTERNACIONAL · B2B INTELLIGENCE136                </div>137            </div>138            <div style="background:#111520;border:1px solid #1E2235;border-radius:6px;139                        padding:2.5rem 2rem 2rem;margin-bottom:1rem;">140                <div style="font-family:'IBM Plex Mono',monospace;font-size:10px;141                            color:#7A85A0;letter-spacing:3px;text-transform:uppercase;142                            margin-bottom:1.2rem;">Access Control</div>143            """,144            unsafe_allow_html=True,145        )146 147        if locked:148            st.error(149                f"⛔  Account temporarily locked · {remaining}s remaining. "150                "Too many failed attempts."151            )152        else:153            pwd       = st.text_input("Master Password", type="password",154                                      placeholder="Enter access key…", key="login_pwd")155            login_btn = st.button("🔓  Unlock Platform", use_container_width=True, key="login_btn")156 157            if login_btn:158                if pwd == "upao":159                    st.session_state["authenticated"] = True160                    st.session_state["login_attempts"] = 0161                    st.rerun()162                else:163                    st.session_state["login_attempts"] += 1164                    attempts = st.session_state["login_attempts"]165                    if attempts >= 3:166                        st.session_state["locked_until"] = time.time() + 30167                        st.error("⛔  3 failed attempts · Locked for 30 seconds.")168                    else:169                        st.error(f"❌  Incorrect password · {3 - attempts} attempt(s) remaining.")170 171        st.markdown("</div>", unsafe_allow_html=True)172        st.markdown(173            """<div style="text-align:center;font-family:'IBM Plex Mono',monospace;174                font-size:9px;color:#2C3248;margin-top:1.5rem;letter-spacing:2px;">175                CQantion-Internacional · v2.0.0 · Commercial Release176            </div>""",177            unsafe_allow_html=True,178        )179 180    return False181 182 183# ═════════════════════════════════════════════════════════════════════184# DIRECTIVA 4 — PROXY ROTATION ARCHITECTURE185# ═════════════════════════════════════════════════════════════════════186#187#  ┌─ CÓMO INYECTAR CREDENCIALES DE PROXY RESIDENCIAL ─────────────────188#  │189#  │  Una vez adquieras tu proveedor (BrightData, Oxylabs, Smartproxy,190#  │  IPRoyal, Rayobyte…), solo exportas UNA variable de entorno antes191#  │  de lanzar la app. Cero cambios de código.192#  │193#  │  Proxy único:194#  │    export CQI_PROXY_URL="http://usuario:contraseña@gateway:puerto"195#  │196#  │  Pool de proxies con rotación aleatoria:197#  │    export CQI_PROXY_LIST="http://u:p@p1:port,http://u:p@p2:port"198#  │199#  │  Deshabilitar sin borrar la variable:200#  │    export CQI_PROXY_ENABLED="false"201#  │202#  │  MECANISMO DE PROPAGACIÓN:203#  │  ProxyManager setea HTTP_PROXY / HTTPS_PROXY en el entorno del204#  │  proceso, por lo que TODOS los módulos de backend que usan205#  │  requests.get() heredan el proxy automáticamente.206#  │  No hay que modificar ningún archivo de backend.207#  └───────────────────────────────────────────────────────────────────208 209class ProxyManager:210    """211    Centralised proxy management. Reads exclusively from env vars.212    Propagates to os.environ so all backend requests calls inherit it.213    """214    _ENABLED = "CQI_PROXY_ENABLED"215    _SINGLE  = "CQI_PROXY_URL"216    _POOL    = "CQI_PROXY_LIST"217    _TIMEOUT = "CQI_PROXY_TIMEOUT"218 219    def __init__(self) -> None:220        enabled_raw  = os.environ.get(self._ENABLED, "true").lower()221        self.enabled = enabled_raw not in ("false", "0", "no", "off")222 223        self._pool: list = []224        single   = os.environ.get(self._SINGLE, "").strip()225        pool_raw = os.environ.get(self._POOL, "").strip()226 227        if pool_raw:228            self._pool = [p.strip() for p in pool_raw.split(",") if p.strip()]229        elif single:230            self._pool = [single]231 232        self.timeout = int(os.environ.get(self._TIMEOUT, "10"))233 234        # Propagate so backend modules pick up proxy automatically235        if self.enabled and self._pool:236            chosen = random.choice(self._pool)237            os.environ["HTTPS_PROXY"] = chosen238            os.environ["HTTP_PROXY"]  = chosen239 240    @property241    def active(self) -> bool:242        return self.enabled and len(self._pool) > 0243 244    def get_proxies(self) -> Optional[dict]:245        """246        Returns a requests-compatible proxy dict (rotates randomly),247        or None when no proxy is configured.248 249        Usage in any new requests call:250            resp = requests.get(url, proxies=PROXY_MGR.get_proxies(), timeout=10)251        """252        if not self.active:253            return None254        chosen = random.choice(self._pool)255        return {"http": chosen, "https": chosen}256 257    def status_label(self) -> str:258        if self.active:259            return f"ACTIVE · {len(self._pool)} proxy/proxies"260        return "INACTIVE · No proxies configured"261 262 263# Singleton — instantiated once at module load264PROXY_MGR = ProxyManager()265 266 267# ═════════════════════════════════════════════════════════════════════268# DIRECTIVA 3 — FIREWALL / ERROR INTERCEPTION269# ═════════════════════════════════════════════════════════════════════270 271class FirewallBlockedError(Exception):272    """Domain is actively blocking scraping (403, Cloudflare, WAF)."""273 274 275class RateLimitedError(Exception):276    """Domain is rate-limiting requests (429)."""277 278 279_FIREWALL_PATTERNS = [280    "cf-browser-verification", "cloudflare", "enable javascript and cookies",281    "just a moment", "ddos protection", "access denied", "403 forbidden",282    "this site is protected", "firewall", "bot protection",283    "_cf_chl", "challenge-platform",284]285 286_BLOCKED_CODES = {403, 406, 429, 503}287 288_UA_POOL = [289    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "290    "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",291    "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4_1) AppleWebKit/605.1.15 "292    "(KHTML, like Gecko) Version/17.4 Safari/605.1.15",293    "Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0",294]295 296 297def safe_get(url: str, timeout: int = 10) -> Optional[requests.Response]:298    """299    requests.get() wrapper with:300      ✔ Random User-Agent rotation301      ✔ Proxy injection via ProxyManager302      ✔ Firewall / WAF body detection303      ✔ Clean exception surface for UI layer304 305    Raises FirewallBlockedError or RateLimitedError for blocked requests.306    Returns None on network-level failures (timeout, DNS).307    """308    headers = {309        "User-Agent":      random.choice(_UA_POOL),310        "Accept":          "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",311        "Accept-Language": "en-US,en;q=0.5",312        "Accept-Encoding": "gzip, deflate",313        "Connection":      "keep-alive",314    }315    try:316        resp = requests.get(317            url,318            headers=headers,319            proxies=PROXY_MGR.get_proxies(),   # None = no proxy; dict = routed320            timeout=timeout,321            allow_redirects=True,322        )323    except (requests.exceptions.Timeout,324            requests.exceptions.ConnectionError,325            requests.exceptions.RequestException):326        return None327 328    if resp.status_code == 429:329        raise RateLimitedError(f"429 Too Many Requests · {url}")330    if resp.status_code in _BLOCKED_CODES:331        raise FirewallBlockedError(f"HTTP {resp.status_code} · {url}")332 333    if resp.status_code == 200:334        body = resp.text[:3000].lower()335        for pat in _FIREWALL_PATTERNS:336            if pat in body:337                raise FirewallBlockedError(338                    f"WAF/Cloudflare challenge detected (pattern: '{pat}') · {url}"339                )340 341    return resp342 343 344def show_firewall_alert(domain: str, detail: str = "") -> None:345    """Standardised anti-fricción firewall warning card."""346    st.warning(347        f"**⛔ Acceso denegado: El firewall del cliente bloqueó la extracción de datos**  \n"348        f"`{domain}` — El servidor rechazó la conexión (Cloudflare / WAF activo).  \n"349        f"Solución: activa proxies residenciales (configura `CQI_PROXY_URL`).  \n"350        + (f"`{detail}`" if detail else "")351    )352 353 354def show_ratelimit_alert(domain: str) -> None:355    """Standardised rate-limit warning."""356    st.warning(357        f"**⏱ Rate Limit detectado: `{domain}`**  \n"358        f"El servidor devolvió 429. Activa rotación de proxies o aumenta el delay."359    )360 361 362# ─────────────────────────────────────────────────────────────────────363# SAFE MODULE IMPORTS — graceful degradation364# ─────────────────────────────────────────────────────────────────────365MODULE_STATUS: dict = {}366 367try:368    from antigravity_competitive_intel import (369        fetch_sitemap, parse_sitemap, score_tier, _clean_domain,370    )371    MODULE_STATUS["intel"] = True372except Exception as exc:373    MODULE_STATUS["intel"] = str(exc)374 375try:376    from shopify_lead_extractor import ShopifyLeadExtractor377    MODULE_STATUS["extractor"] = True378except Exception as exc:379    MODULE_STATUS["extractor"] = str(exc)380 381try:382    from antigravity_email_validator import EmailValidator383    MODULE_STATUS["validator"] = True384except Exception as exc:385    MODULE_STATUS["validator"] = str(exc)386 387try:388    from advanced_extractor import AdvancedShopifyExtractor389    MODULE_STATUS["advanced"] = True390except Exception as exc:391    MODULE_STATUS["advanced"] = str(exc)392 393try:394    from antigravity_deep_personalizer import scrape_tienda, normalizar_url395    MODULE_STATUS["personalizer"] = True396except Exception as exc:397    MODULE_STATUS["personalizer"] = str(exc)398 399MODULE_STATUS["motor_robusto"]    = "CLI_ONLY"400MODULE_STATUS["config"]           = True401MODULE_STATUS["ejemplo_practico"] = True402MODULE_STATUS["test_system"]      = True403 404 405# ─────────────────────────────────────────────────────────────────────406# DESIGN TOKENS & SHARED HELPERS407# ─────────────────────────────────────────────────────────────────────408TIER_COLORS = {"A": "#00FFB2", "B": "#FFB020", "C": "#7A85A0"}409TIER_LABELS = {"A": "BIG FISH", "B": "NURTURE", "C": "DISCARD"}410 411 412def tier_badge(tier: str) -> str:413    cls   = f"tier-{tier.lower()}"414    label = TIER_LABELS.get(tier, tier)415    return f'<span class="tier-badge {cls}">{tier} · {label}</span>'416 417 418def parse_domain_input(raw: str) -> list:419    parts   = re.split(r"[\n,;\s]+", raw.strip())420    domains = []421    for p in parts:422        p = p.strip()423        if not p:424            continue425        for prefix in ("https://", "http://"):426            if p.startswith(prefix):427                p = p[len(prefix):]428        p = p.rstrip("/")429        if p:430            domains.append(p)431    return domains432 433 434def log_line(ph, msg: str, color: str = "#7A85A0") -> None:435    ph.markdown(436        f'<div style="font-family:IBM Plex Mono,monospace;font-size:12px;'437        f'color:{color};padding:3px 0;">▶ {msg}</div>',438        unsafe_allow_html=True,439    )440 441 442def module_required_error(key: str, pip_hint: str = "") -> None:443    msg = MODULE_STATUS.get(key, "Unknown error")444    st.markdown(445        f'<div class="cq-card cq-card-red">'446        f'<div style="font-family:var(--mono);font-size:12px;color:var(--red);margin-bottom:6px;">⚠ MODULE OFFLINE</div>'447        f'<div style="font-family:var(--mono);font-size:11px;color:var(--muted);">{msg}</div>'448        + (f'<div style="font-family:var(--mono);font-size:11px;color:var(--dim);margin-top:8px;">pip install {pip_hint}</div>' if pip_hint else "")449        + '</div>',450        unsafe_allow_html=True,451    )452 453 454def html_table(headers: list, rows: list) -> str:455    ths = "".join(456        f'<th style="padding:10px 14px;text-align:left;font-family:IBM Plex Mono,monospace;'457        f'font-size:10px;letter-spacing:2.5px;color:#7A85A0;text-transform:uppercase;'458        f'white-space:nowrap;">{h}</th>'459        for h in headers460    )461    trs = "".join(462        "<tr>" + "".join(463            f'<td style="padding:10px 14px;border-bottom:1px solid #1E2235;">{c}</td>'464            for c in cells465        ) + "</tr>"466        for cells in rows467    )468    return (469        '<div style="background:#111520;border:1px solid #1E2235;border-radius:5px;'470        'overflow:hidden;max-height:520px;overflow-y:auto;overflow-x:auto;">'471        f'<table style="width:100%;border-collapse:collapse;min-width:480px;">'472        f'<thead><tr style="background:#0B0E18;border-bottom:2px solid #1E2235;">{ths}</tr></thead>'473        f'<tbody>{trs}</tbody></table></div>'474    )475 476 477def csv_download_row(df: pd.DataFrame, label: str, key: str) -> None:478    """479    DIRECTIVA 1 — Standardised export block rendered below every result table.480    UTF-8-BOM encoding so Excel on Windows renders tildes and ñ correctly.481    """482    st.markdown(483        '<div style="display:flex;align-items:center;gap:10px;margin-top:0.75rem;'484        'padding-top:0.75rem;border-top:1px solid #1E2235;">'485        '<span style="font-family:IBM Plex Mono,monospace;font-size:9px;'486        'color:#7A85A0;letter-spacing:3px;text-transform:uppercase;">Export Results</span>'487        '</div>',488        unsafe_allow_html=True,489    )490    col_btn, col_info = st.columns([2, 3])491    with col_btn:492        st.download_button(493            label=f"↓ Download {label}.csv",494            data=df.to_csv(index=False, encoding="utf-8-sig").encode("utf-8-sig"),495            file_name=f"cqi_{label}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",496            mime="text/csv",497            key=key,498            use_container_width=True,499        )500    with col_info:501        st.markdown(502            f'<div style="font-family:IBM Plex Mono,monospace;font-size:10px;color:#2C3248;'503            f'padding-top:0.6rem;">{len(df):,} rows · UTF-8-BOM · Excel compatible</div>',504            unsafe_allow_html=True,505        )506 507 508# ─────────────────────────────────────────────────────────────────────509# CSS — CQantion dark precision theme (Syne + IBM Plex Mono + DM Sans)510# ─────────────────────────────────────────────────────────────────────511def inject_css() -> None:512    st.markdown(513        """514        <link rel="preconnect" href="https://fonts.googleapis.com">515        <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600&family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">516 517        <style>518        :root {519            --bg:      #04040A;520            --surface: #0B0E18;521            --card:    #111520;522            --border:  #1E2235;523            --accent:  #00FFB2;524            --blue:    #5B9BFF;525            --amber:   #FFB020;526            --red:     #FF4D6A;527            --text:    #DDE4F0;528            --muted:   #7A85A0;529            --dim:     #2C3248;530            --mono:    'IBM Plex Mono', monospace;531            --body:    'DM Sans', sans-serif;532            --display: 'Syne', sans-serif;533        }534        .stApp,535        [data-testid="stAppViewContainer"],536        [data-testid="stMain"] {537            background-color: var(--bg) !important;538            font-family: var(--body) !important;539            color: var(--text) !important;540        }541        [data-testid="stHeader"]  { background: transparent !important; }542        [data-testid="stSidebar"] {543            background-color: var(--surface) !important;544            border-right: 1px solid var(--border) !important;545        }546        [data-testid="stSidebarContent"] { padding:1.5rem 1rem !important; }547        h1,h2,h3,h4 { font-family:var(--display) !important; color:var(--text) !important; }548        p, li { font-family:var(--body) !important; color:var(--text) !important; }549        label,[data-testid="stWidgetLabel"] {550            font-family:var(--mono) !important;551            font-size:11px !important;552            letter-spacing:1.5px !important;553            text-transform:uppercase !important;554            color:var(--muted) !important;555        }556        .stTextInput > div > div > input,557        .stTextArea > div > div > textarea {558            background-color:var(--card) !important;559            border:1px solid var(--border) !important;560            color:var(--text) !important;561            font-family:var(--mono) !important;562            font-size:13px !important;563            border-radius:4px !important;564        }565        .stTextInput > div > div > input:focus,566        .stTextArea > div > div > textarea:focus {567            border-color:var(--accent) !important;568            box-shadow:0 0 0 1px rgba(0,255,178,0.3) !important;569        }570        .stTextInput > div > div > input::placeholder,571        .stTextArea > div > div > textarea::placeholder { color:var(--dim) !important; }572        .stButton > button {573            background:var(--accent) !important;574            color:#020209 !important;575            font-family:var(--display) !important;576            font-weight:800 !important;577            font-size:13px !important;578            letter-spacing:2.5px !important;579            text-transform:uppercase !important;580            border:none !important;581            border-radius:4px !important;582            padding:0.65rem 2rem !important;583            transition:background 0.15s,box-shadow 0.15s,transform 0.1s !important;584        }585        .stButton > button:hover {586            background:#00CCAA !important;587            box-shadow:0 0 28px rgba(0,255,178,0.35) !important;588            transform:translateY(-1px) !important;589        }590        .stButton > button:active { transform:translateY(0) !important; }591        .stDownloadButton > button {592            background:transparent !important;593            color:var(--accent) !important;594            border:1px solid rgba(0,255,178,0.35) !important;595            font-family:var(--mono) !important;596            font-size:11px !important;597            letter-spacing:1.5px !important;598            border-radius:4px !important;599            padding:0.5rem 1.2rem !important;600            transition:all 0.15s !important;601        }602        .stDownloadButton > button:hover {603            background:rgba(0,255,178,0.08) !important;604            border-color:var(--accent) !important;605            box-shadow:0 0 12px rgba(0,255,178,0.2) !important;606        }607        [data-testid="stTabs"] [data-baseweb="tab-list"] {608            background:transparent !important;609            border-bottom:1px solid var(--border) !important;610            gap:0 !important;611        }612        [data-testid="stTabs"] [data-baseweb="tab"] {613            background:transparent !important;614            color:var(--muted) !important;615            font-family:var(--mono) !important;616            font-size:11px !important;617            letter-spacing:2px !important;618            text-transform:uppercase !important;619            padding:12px 18px !important;620            border-bottom:2px solid transparent !important;621            transition:color 0.15s !important;622        }623        [data-testid="stTabs"] [data-baseweb="tab"]:hover { color:var(--text) !important; }624        [data-testid="stTabs"] [data-baseweb="tab"][aria-selected="true"] {625            color:var(--accent) !important;626            border-bottom-color:var(--accent) !important;627        }628        [data-testid="stTabs"] [data-baseweb="tab-panel"] { padding-top:2rem !important; }629        [data-baseweb="tab-highlight"] { background-color:var(--accent) !important; }630        [data-testid="stMetric"] {631            background:var(--card) !important;632            border:1px solid var(--border) !important;633            border-radius:5px !important;634            padding:1.1rem 1.2rem !important;635        }636        [data-testid="stMetricValue"] {637            font-family:var(--mono) !important;638            color:var(--accent) !important;639            font-size:2.2rem !important;640            line-height:1.1 !important;641        }642        [data-testid="stMetricLabel"] {643            font-family:var(--mono) !important;644            color:var(--muted) !important;645            font-size:10px !important;646            letter-spacing:2.5px !important;647            text-transform:uppercase !important;648        }649        [data-testid="stProgressBar"] > div > div {650            background:linear-gradient(90deg,var(--accent),#00CCAA) !important;651        }652        [data-testid="stProgressBar"] > div {653            background:var(--card) !important;654            border-radius:2px !important;655        }656        [data-testid="stFileUploader"] {657            background:var(--card) !important;658            border:1px dashed var(--dim) !important;659            border-radius:5px !important;660        }661        [data-testid="stCheckbox"] span {662            font-family:var(--mono) !important;663            font-size:12px !important;664            color:var(--muted) !important;665        }666        [data-testid="stExpander"] {667            background:var(--card) !important;668            border:1px solid var(--border) !important;669            border-radius:5px !important;670        }671        [data-testid="stExpander"] summary {672            font-family:var(--mono) !important;673            font-size:11px !important;674            letter-spacing:1.5px !important;675            text-transform:uppercase !important;676            color:var(--muted) !important;677        }678        [data-testid="stAlert"] {679            background:var(--card) !important;680            border-radius:5px !important;681            font-family:var(--body) !important;682            font-size:13px !important;683        }684        ::-webkit-scrollbar        { width:5px; height:5px; }685        ::-webkit-scrollbar-track  { background:var(--bg); }686        ::-webkit-scrollbar-thumb  { background:var(--border); border-radius:3px; }687        ::-webkit-scrollbar-thumb:hover { background:var(--dim); }688        .cq-card {689            background:var(--card);690            border:1px solid var(--border);691            border-radius:5px;692            padding:1.25rem 1.5rem;693            margin-bottom:1.75rem;694        }695        .cq-card-accent { border-left:3px solid var(--accent); }696        .cq-card-blue   { border-left:3px solid var(--blue); }697        .cq-card-amber  { border-left:3px solid var(--amber); }698        .cq-card-red    { border-left:3px solid var(--red); }699        .cq-header {700            display:flex;701            align-items:flex-end;702            justify-content:space-between;703            border-bottom:1px solid var(--border);704            padding-bottom:1.25rem;705            margin-bottom:2rem;706        }707        .cq-logo {708            font-family:var(--display);709            font-size:30px;710            font-weight:800;711            letter-spacing:2px;712            color:var(--text);713            line-height:1;714        }715        .cq-logo .hi     { color:var(--accent); }716        .cq-logo .suffix {717            font-size:11px;718            letter-spacing:4px;719            color:var(--muted);720            font-weight:400;721            font-family:var(--mono);722            margin-left:10px;723            vertical-align:middle;724        }725        .cq-tagline {726            font-family:var(--mono);727            font-size:10px;728            color:var(--muted);729            letter-spacing:3px;730            text-transform:uppercase;731            margin-top:5px;732        }733        .cq-ts { font-family:var(--mono); font-size:10px; color:var(--dim); letter-spacing:1px; }734        .tier-badge {735            display:inline-block;736            padding:3px 10px 2px;737            border-radius:3px;738            font-family:var(--mono);739            font-size:11px;740            font-weight:600;741            letter-spacing:1.5px;742        }743        .tier-a { background:rgba(0,255,178,0.09);  color:#00FFB2; border:1px solid rgba(0,255,178,0.3); }744        .tier-b { background:rgba(255,176,32,0.09);  color:#FFB020; border:1px solid rgba(255,176,32,0.3); }745        .tier-c { background:rgba(122,133,160,0.09); color:#7A85A0; border:1px solid rgba(122,133,160,0.25); }746        .status-dot {747            display:inline-block;748            width:7px; height:7px;749            border-radius:50%;750            margin-right:5px;751            vertical-align:middle;752        }753        .dot-green { background:#00FFB2; box-shadow:0 0 6px rgba(0,255,178,0.7); }754        .dot-red   { background:#FF4D6A; }755        .dot-amber { background:#FFB020; box-shadow:0 0 5px rgba(255,176,32,0.5); }756        .section-label {757            font-family:var(--mono);758            font-size:9px;759            color:var(--muted);760            letter-spacing:3px;761            text-transform:uppercase;762            margin-bottom:0.5rem;763        }764        .proxy-badge {765            display:inline-block;766            padding:2px 8px;767            border-radius:3px;768            font-family:var(--mono);769            font-size:10px;770            letter-spacing:1px;771        }772        .proxy-on  { background:rgba(0,255,178,0.09);color:#00FFB2;border:1px solid rgba(0,255,178,0.3); }773        .proxy-off { background:rgba(255,176,32,0.09);color:#FFB020;border:1px solid rgba(255,176,32,0.3); }774        #MainMenu, footer, [data-testid="stToolbar"] { display:none !important; }775        </style>776        """,777        unsafe_allow_html=True,778    )779 780 781# ─────────────────────────────────────────────────────────────────────782# SIDEBAR783# ─────────────────────────────────────────────────────────────────────784def _sec(text: str) -> None:785    st.markdown(786        f'<div style="font-family:IBM Plex Mono,monospace;font-size:9px;color:#7A85A0;'787        f'letter-spacing:3px;text-transform:uppercase;margin-bottom:8px;">{text}</div>',788        unsafe_allow_html=True,789    )790 791 792def render_sidebar() -> None:793    with st.sidebar:794        st.markdown(795            """<div style="padding:0.5rem 0 1.25rem;border-bottom:1px solid #1E2235;margin-bottom:1.25rem;">796                <div style="font-family:'Syne',sans-serif;font-size:20px;font-weight:800;797                            letter-spacing:2px;color:#DDE4F0;">798                    CQ<span style="color:#00FFB2;">antion</span>799                </div>800                <div style="font-family:'IBM Plex Mono',monospace;font-size:9px;801                            color:#7A85A0;letter-spacing:3px;margin-top:2px;">INTERNACIONAL</div>802            </div>""",803            unsafe_allow_html=True,804        )805 806        if st.session_state.get("authenticated"):807            if st.button("🔒 Lock Session", use_container_width=True, key="logout_btn"):808                st.session_state["authenticated"] = False809                st.rerun()810            st.markdown("<div style='margin-bottom:1.25rem'></div>", unsafe_allow_html=True)811 812        _sec("System Status")813        for label, key in {814            "Intel Scanner":    "intel",815            "Lead Extractor":   "extractor",816            "Email Validator":  "validator",817            "Adv. Extractor":   "advanced",818            "Deep Personalizer":"personalizer",819            "Motor Robusto":    "motor_robusto",820        }.items():821            s  = MODULE_STATUS.get(key)822            ok = s is True;  cli = s == "CLI_ONLY"823            dc = "dot-green" if ok else ("dot-amber" if cli else "dot-red")824            tc = "#00FFB2"   if ok else ("#FFB020"   if cli else "#FF4D6A")825            tv = "ONLINE"    if ok else ("CLI"       if cli else "OFFLINE")826            st.markdown(827                f'<div style="display:flex;justify-content:space-between;align-items:center;'828                f'padding:5px 0;border-bottom:1px solid #1E2235;">'829                f'<span style="font-family:IBM Plex Mono,monospace;font-size:11px;color:#7A85A0;">{label}</span>'830                f'<span><span class="status-dot {dc}"></span>'831                f'<span style="font-family:IBM Plex Mono,monospace;font-size:10px;color:{tc};">{tv}</span></span></div>',832                unsafe_allow_html=True,833            )834 835        st.markdown("<br>", unsafe_allow_html=True)836        _sec("Proxy Status")837        pcls = "proxy-on" if PROXY_MGR.active else "proxy-off"838        st.markdown(839            f'<div style="margin-bottom:0.5rem;">'840            f'<span class="proxy-badge {pcls}">{PROXY_MGR.status_label()}</span></div>',841            unsafe_allow_html=True,842        )843        if not PROXY_MGR.active:844            st.markdown(845                '<div style="font-family:IBM Plex Mono,monospace;font-size:9px;color:#2C3248;'846                'line-height:1.6;">Set CQI_PROXY_URL to activate</div>',847                unsafe_allow_html=True,848            )849 850        st.markdown("<br>", unsafe_allow_html=True)851        _sec("AI Config")852        gk = st.text_input("Gemini API Key", type="password",853                           placeholder="AIza…", key="gemini_key_input")854        if gk:855            os.environ["GEMINI_API_KEY"] = gk856            st.success("✔ Key saved to session.")857 858        st.markdown(859            '<div style="margin-top:2rem;padding-top:1rem;border-top:1px solid #1E2235;'860            'text-align:center;font-family:IBM Plex Mono,monospace;font-size:9px;color:#2C3248;">'861            'CQantion-Internacional · v2.0.0</div>',862            unsafe_allow_html=True,863        )864 865 866# ─────────────────────────────────────────────────────────────────────867# HEADER868# ─────────────────────────────────────────────────────────────────────869def render_header() -> None:870    proxy_badge = (871        '<span class="proxy-badge proxy-on" style="font-size:9px;margin-left:12px;">PROXY ON</span>'872        if PROXY_MGR.active else ""873    )874    st.markdown(875        f"""<div class="cq-header">876            <div>877                <div class="cq-logo">878                    CQ<span class="hi">antion</span>879                    <span class="suffix">INTERNACIONAL</span>880                    {proxy_badge}881                </div>882                <div class="cq-tagline">B2B Intelligence Platform · Shopify Market Scanner · v2.0</div>883            </div>884            <div class="cq-ts">{datetime.now().strftime("%Y-%m-%d  %H:%M")}</div>885        </div>""",886        unsafe_allow_html=True,887    )888 889 890def tab_header(title: str, subtitle: str, color: str = "var(--accent)") -> None:891    st.markdown(892        f'<div class="cq-card" style="border-left:3px solid {color};margin-bottom:1.5rem;">'893        f'<div style="font-family:Syne,sans-serif;font-size:20px;font-weight:700;'894        f'letter-spacing:2px;color:#DDE4F0;margin-bottom:3px;">{title}</div>'895        f'<div style="font-family:IBM Plex Mono,monospace;font-size:11px;color:#7A85A0;">{subtitle}</div>'896        f'</div>',897        unsafe_allow_html=True,898    )899 900 901# ═════════════════════════════════════════════════════════════════════902# TAB 1 — INTELLIGENCE SCANNER903# ═════════════════════════════════════════════════════════════════════904def render_intel_scanner() -> None:905    tab_header(906        "INTELLIGENCE SCANNER",907        "Shopify Sitemap Analysis · Tier Classification · A = Hot Lead · B = Nurture · C = Discard",908    )909    if MODULE_STATUS.get("intel") is not True:910        module_required_error("intel", "requests pandas beautifulsoup4 lxml"); return911 912    col_in, col_up = st.columns([3, 2])913    with col_in:914        domain_input = st.text_area("Target Domains",915            placeholder="gymshark.com\nalphalete.com\nnobullproject.com",916            height=130, key="intel_domains",917            help="One domain per line · commas or semicolons also work")918    with col_up:919        uploaded_csv = st.file_uploader("or upload CSV (needs TIENDA column)",920                                        type=["csv"], key="intel_csv")921 922    run_btn = st.button("⚡ Execute Intel Scanner", use_container_width=True, key="run_intel")923 924    # Persist results so they survive tab switches925    if not run_btn and st.session_state["intel_results"] is not None:926        _render_intel_table(st.session_state["intel_results"]); return927    if not run_btn:928        return929 930    domains = []931    if uploaded_csv:932        try:933            df_up = pd.read_csv(uploaded_csv, sep=None, engine="python",934                                dtype=str, on_bad_lines="skip")935            df_up.columns = [c.strip() for c in df_up.columns]936            if "TIENDA" not in df_up.columns:937                st.error("CSV must have a 'TIENDA' column."); return938            domains = df_up["TIENDA"].dropna().str.strip().tolist()939        except Exception as exc:940            st.error(f"CSV error: {exc}"); return941    elif domain_input.strip():942        domains = parse_domain_input(domain_input)943    else:944        st.warning("Enter at least one domain."); return945 946    if not domains:947        st.warning("No valid domains found."); return948 949    st.markdown("<br>", unsafe_allow_html=True)950    status_ph = st.empty()951    prog      = st.progress(0)952    total     = len(domains)953    results   = []954    blocked   = []955 956    for i, domain in enumerate(domains):957        clean = _clean_domain(domain)958        log_line(status_ph,959            f"Scanning <b style='color:#DDE4F0'>{clean}</b> "960            f"<span style='color:#5B9BFF'>[{i+1}/{total}]</span>")961        prog.progress(i / max(total, 1))962 963        xml_text   = None964        fw_blocked = False965        fw_detail  = ""966        try:967            resp = safe_get(f"https://{clean}/sitemap_products.1.xml", timeout=8)968            if resp and resp.status_code == 200:969                xml_text = resp.text970        except FirewallBlockedError as e:971            fw_blocked = True; fw_detail = str(e); blocked.append((domain, str(e)))972        except RateLimitedError:973            fw_blocked = True; fw_detail = "429 Rate Limited"; blocked.append((domain, fw_detail))974        except Exception as e:975            fw_detail = str(e)976 977        prods, last_dt = parse_sitemap(xml_text) if xml_text else (0, None)978        tier     = score_tier(prods, last_dt)979        last_str = last_dt.strftime("%Y-%m-%d") if last_dt else "N/A"980 981        results.append({982            "Domain":      clean,983            "Tier":        tier,984            "Products":    prods,985            "Last Update": last_str,986            "Firewall":    "BLOCKED" if fw_blocked else "OK",987        })988 989    prog.progress(1.0)990    log_line(status_ph,991        f"✓ Scan complete · {total} domains · {len(blocked)} blocked", "#00FFB2")992 993    for dom, detail in blocked:994        show_firewall_alert(dom, detail)995 996    df_r = pd.DataFrame(results)997    st.session_state["intel_results"] = df_r998    _render_intel_table(df_r)999 1000 1001def _render_intel_table(df_r: pd.DataFrame) -> None:1002    tc       = df_r["Tier"].value_counts().to_dict()1003    fw_count = int((df_r.get("Firewall", pd.Series()) == "BLOCKED").sum())1004    st.markdown("<br>", unsafe_allow_html=True)1005    c1, c2, c3, c4, c5 = st.columns(5)1006    c1.metric("SCANNED",       len(df_r))1007    c2.metric("TIER A · HOT",  tc.get("A", 0))1008    c3.metric("TIER B · WARM", tc.get("B", 0))1009    c4.metric("TIER C · COLD", tc.get("C", 0))1010    c5.metric("⛔ BLOCKED",    fw_count)1011 1012    st.markdown("<br><div class='section-label'>Scan Results</div>", unsafe_allow_html=True)1013    rows = []1014    for _, row in df_r.sort_values("Tier", key=lambda x: x.map({"A":0,"B":1,"C":2})).iterrows():1015        t  = row["Tier"];  clr = TIER_COLORS.get(t, "#7A85A0")1016        fw = row.get("Firewall", "OK")1017        fw_html = ('<span style="font-family:IBM Plex Mono,monospace;font-size:10px;color:#FF4D6A;">⛔</span>'1018                   if fw == "BLOCKED" else1019                   '<span style="font-family:IBM Plex Mono,monospace;font-size:10px;color:#2C3248;">—</span>')1020        rows.append([1021            f'<span style="font-family:IBM Plex Mono,monospace;font-size:13px;color:#DDE4F0;">{row["Domain"]}</span>',1022            tier_badge(t),1023            f'<span style="font-family:IBM Plex Mono,monospace;font-size:13px;color:{clr};font-weight:600;">{row["Products"]}</span>',1024            f'<span style="font-family:IBM Plex Mono,monospace;font-size:12px;color:#7A85A0;">{row["Last Update"]}</span>',1025            fw_html,1026        ])1027    st.markdown(html_table(["Domain", "Tier", "Products", "Last Updated", "Firewall"], rows),1028                unsafe_allow_html=True)1029    # D1 — Export1030    csv_download_row(df_r, "intel_scan", "dl_intel")1031 1032 1033# ═════════════════════════════════════════════════════════════════════1034# TAB 2 — LEAD EXTRACTOR1035# ═════════════════════════════════════════════════════════════════════1036def render_lead_extractor() -> None:1037    tab_header("LEAD EXTRACTOR",1038               "Contact Page Scraper · Email Harvesting · Generic Address Filter · Firewall Detection",1039               color="var(--blue)")1040 1041    if MODULE_STATUS.get("extractor") is not True:1042        module_required_error("extractor", "requests"); return1043 1044    domain_input = st.text_area("Target Domains",1045        placeholder="gymshark.com\nalphalete.com", height=110, key="extractor_domains",1046        help="Crawler checks /, /pages/contact, /pages/about and similar paths")1047    col_a, col_b = st.columns(2)1048    with col_a:1049        filter_generic = st.checkbox("Filter generic emails", value=True, key="ext_filter")1050    with col_b:1051        show_fw = st.checkbox("Show firewall alerts inline", value=True, key="ext_show_fw")1052 1053    run_btn = st.button("⚡ Extract Leads", use_container_width=True, key="run_extractor")1054 1055    if not run_btn and st.session_state["leads_results"] is not None:1056        _render_leads_table(st.session_state["leads_results"]); return1057    if not run_btn:1058        return1059 1060    domains = parse_domain_input(domain_input)1061    if not domains:1062        st.warning("Enter at least one domain."); return1063 1064    st.markdown("<br>", unsafe_allow_html=True)1065    prog      = st.progress(0)1066    status_ph = st.empty()1067    extractor = ShopifyLeadExtractor(max_stores=len(domains))1068    all_leads = []1069    fw_alerts = []1070    total     = len(domains)1071 1072    for i, domain in enumerate(domains):1073        url = f"https://{domain}" if not domain.startswith("http") else domain1074        log_line(status_ph,1075            f"Extracting from <b style='color:#DDE4F0'>{domain}</b> "1076            f"<span style='color:#5B9BFF'>[{i+1}/{total}]</span>")1077        prog.progress(i / max(total, 1))1078 1079        # D3 — Firewall pre-check1080        try:1081            probe = safe_get(url, timeout=8)1082            if probe is None:1083                log_line(status_ph, f"  ⚠ {domain} — No response", "#FFB020"); continue1084        except FirewallBlockedError as e:1085            fw_alerts.append((domain, str(e)))1086            log_line(status_ph, f"  ⛔ {domain} — Firewall blocked", "#FF4D6A")1087            if show_fw: show_firewall_alert(domain, str(e))1088            continue1089        except RateLimitedError:1090            fw_alerts.append((domain, "429"))1091            log_line(status_ph, f"  ⏱ {domain} — Rate limited", "#FF4D6A")1092            if show_fw: show_ratelimit_alert(domain)1093            continue1094 1095        try:1096            contact = extractor._extract_contact_info(url)1097            emails  = contact.get("emails", [])1098            if filter_generic:1099                emails = [e for e in emails if not extractor._is_generic_email(e)]1100            for email in emails:1101                all_leads.append({1102                    "Domain":     domain,1103                    "Store Name": contact.get("store_name", ""),1104                    "Email":      email,1105                    "Extracted":  datetime.now().strftime("%Y-%m-%d %H:%M:%S"),1106                })1107        except Exception as exc:1108            err = str(exc).lower()1109            if any(k in err for k in ("403","cloudflare","blocked","forbidden","access denied")):1110                fw_alerts.append((domain, str(exc)))1111                if show_fw: show_firewall_alert(domain, str(exc))1112            else:1113                st.error(f"Error on `{domain}`: {exc}")1114 1115    prog.progress(1.0)1116    log_line(status_ph,1117        f"✓ Done · {len(all_leads)} emails · {len(fw_alerts)} blocked", "#00FFB2")1118 1119    if not all_leads:1120        st.info("No emails found. Possible causes:\n"1121                "- Cloudflare/WAF blocking (activate proxies)\n"1122                "- No public contact pages\n"1123                "- All emails filtered as generic (try disabling the filter)")1124        return1125 1126    df_leads = pd.DataFrame(all_leads)1127    st.session_state["leads_results"] = df_leads1128    _render_leads_table(df_leads)1129 1130 1131def _render_leads_table(df_leads: pd.DataFrame) -> None:1132    st.markdown("<br>", unsafe_allow_html=True)1133    c1, c2 = st.columns(2)1134    c1.metric("EMAILS FOUND", len(df_leads))1135    c2.metric("DOMAINS HIT",  df_leads["Domain"].nunique())1136    st.markdown("<br><div class='section-label'>Extracted Leads</div>", unsafe_allow_html=True)1137    rows = [1138        [1139            f'<span style="font-family:IBM Plex Mono,monospace;font-size:12px;color:#7A85A0;">{r["Domain"]}</span>',1140            f'<span style="font-family:IBM Plex Mono,monospace;font-size:13px;color:#5B9BFF;">{r["Email"]}</span>',1141            f'<span style="font-family:IBM Plex Mono,monospace;font-size:12px;color:#7A85A0;">{r["Store Name"]}</span>',1142            f'<span style="font-family:IBM Plex Mono,monospace;font-size:10px;color:#2C3248;">{r["Extracted"]}</span>',1143        ]1144        for _, r in df_leads.iterrows()1145    ]1146    st.markdown(html_table(["Domain", "Email", "Store Name", "Timestamp"], rows),1147                unsafe_allow_html=True)1148    # D1 — Export1149    csv_download_row(df_leads, "leads", "dl_leads")1150 1151 1152# ═════════════════════════════════════════════════════════════════════1153# TAB 3 — EMAIL VALIDATOR1154# ═════════════════════════════════════════════════════════════════════1155def render_email_validator() -> None:1156    tab_header("EMAIL VALIDATOR",1157               "3-Layer Pipeline · Syntax → DNS/MX Lookup → Disposable Blacklist · Zero SMTP",1158               color="var(--amber)")1159 1160    if MODULE_STATUS.get("validator") is not True:1161        module_required_error("validator", "dnspython pandas"); return1162 1163    col_in, col_up = st.columns([3, 2])1164    with col_in:1165        email_input = st.text_area("Emails to validate",1166            placeholder="founder@brand.com\nceo@startup.io\ninfo@shop.com",1167            height=140, key="val_emails")1168    with col_up:1169        uploaded_csv = st.file_uploader(1170            "or upload CSV (needs EMAIL or CORREO column)", type=["csv"], key="val_csv")1171 1172    run_btn = st.button("⚡ Run Validation Pipeline", use_container_width=True, key="run_validator")1173 1174    if not run_btn and st.session_state["valid_results"] is not None:1175        _render_validator_table(*st.session_state["valid_results"]); return1176    if not run_btn:1177        return1178 1179    emails: list = []1180    if uploaded_csv:1181        try:1182            df_up = pd.read_csv(uploaded_csv, sep=None, engine="python",1183                                dtype=str, on_bad_lines="skip")1184            df_up.columns = [c.strip().upper() for c in df_up.columns]1185            col_e = next((c for c in ["EMAIL","CORREO"] if c in df_up.columns), None)1186            if not col_e:1187                st.error("CSV must have an 'EMAIL' or 'CORREO' column."); return1188            emails = df_up[col_e].dropna().str.strip().tolist()1189        except Exception as exc:1190            st.error(f"CSV error: {exc}"); return1191    elif email_input.strip():1192        emails = [e.strip() for e in email_input.strip().splitlines() if e.strip()]1193    else:1194        st.warning("Enter emails or upload a CSV."); return1195 1196    if not emails:1197        st.warning("No emails to validate."); return1198 1199    st.markdown("<br>", unsafe_allow_html=True)1200    prog      = st.progress(0)

Showing the first 1,200 of 1584 lines. Download the file for the rest.