diff --git a/send_slack_report.py b/send_slack_report.py index 518a35d..56f59e6 100644 --- a/send_slack_report.py +++ b/send_slack_report.py @@ -17,7 +17,8 @@ def _extract_vertical(name: str) -> str: def main(): - run_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") + import sys as _sys + run_date = _sys.argv[1] if len(_sys.argv) > 1 else (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") print(f"Reenviando informe para {run_date}...") meta = MetaAdsClient() diff --git a/slack_notifier.py b/slack_notifier.py index d77725c..9fee718 100644 --- a/slack_notifier.py +++ b/slack_notifier.py @@ -57,7 +57,10 @@ def _post(method: str, **payload) -> dict: json=payload, timeout=10, ) - return resp.json() + data = resp.json() + if not data.get("ok"): + raise RuntimeError(f"Slack {method} error: {data.get('error')} — {data.get('response_metadata', {}).get('messages', '')}") + return data def update_message(channel: str, ts: str, text: str): @@ -253,15 +256,23 @@ def send_daily_report( }) blocks.append({"type": "divider"}) + blocks.append({ + "type": "context", + "elements": [{"type": "mrkdwn", + "text": f"{campaigns_analyzed} campañas analizadas — detalle por campaña a continuación"}], + }) - # ── Análisis por campaña: decisión + adsets + anuncios ──────────────────── + # ── Enviar resumen ──────────────────────────────────────────────────────── + result = _post( + "chat.postMessage", + channel=config.SLACK_CHANNEL_ID, + blocks=blocks, + text=f"Meta Optimizer — {prefix} — {date_label}", + ) + ts = result.get("ts") + + # ── Un mensaje por campaña (sin límite de bloques) ──────────────────────── if campaign_details: - blocks.append({ - "type": "section", - "text": {"type": "mrkdwn", - "text": f"*Análisis por campaña ({campaigns_analyzed} activas ayer)*"}, - }) - # Build lookup: campaign_name → action info action_map = {a["campaign_name"]: a for a in actions} for cid, detail in campaign_details.items(): @@ -279,11 +290,9 @@ def send_daily_report( budget = bid_cfg.get("daily_budget_eur") budget_str = f"{budget:.0f}€/día" if budget else "—" - # Icono y etiqueta de acción atype = action["action_type"] if action else "MAINTAIN" emoji, alabel = _ACTION_DISPLAY.get(atype, ("⚪", atype)) - # Campaign header header_text = ( f"{emoji} *{name}*\n" f"Vertical: _{vertical}_ | Margen: *{m_str}* | " @@ -291,25 +300,23 @@ def send_daily_report( f"*Decisión: {alabel}*" ) if action: - justification = action.get("justification", "")[:500] - header_text += f"\n_{justification}_" + header_text += f"\n_{action.get('justification', '')[:500]}_" if action.get("alert"): header_text += f"\n:warning: {action['alert']}" - blocks.append({ - "type": "section", - "text": {"type": "mrkdwn", "text": header_text}, - }) + camp_blocks: list = [ + {"type": "section", "text": {"type": "mrkdwn", "text": header_text}}, + ] - # Botones solo para acciones que ejecutan algo en Meta API + # Botones de acción a nivel campaña if action and atype in _ACTIONABLE: effect = _effect_text(action, budget) if effect: - blocks.append({ + camp_blocks.append({ "type": "section", "text": {"type": "mrkdwn", "text": effect}, }) - blocks.append({ + camp_blocks.append({ "type": "actions", "elements": [ { @@ -330,48 +337,26 @@ def send_daily_report( }) # Tabla adsets + anuncios - detail_parts = [] adset_table = _adset_ad_table(adsets, "Conjuntos de anuncios", show_bid=True) ad_table = _adset_ad_table(ads, "Anuncios") - if adset_table: - detail_parts.append(adset_table) - if ad_table: - detail_parts.append(ad_table) - if detail_parts: - combined = "\n\n".join(detail_parts) - for chunk in [combined[i:i+2900] for i in range(0, len(combined), 2900)]: - blocks.append({ - "type": "section", - "text": {"type": "mrkdwn", "text": chunk}, - }) - - # Botones de pausa por anuncio (solo los que Claude recomienda pausar) - blocks.extend(_ad_action_blocks(ads)) - - blocks.append({"type": "divider"}) - - if len(blocks) >= 45: # Safety margin before Slack's 50-block limit - blocks.append({ + combined = "\n\n".join(filter(None, [adset_table, ad_table])) + for chunk in [combined[i:i+2900] for i in range(0, len(combined), 2900)]: + camp_blocks.append({ "type": "section", - "text": {"type": "mrkdwn", - "text": "_... más campañas disponibles en Baserow._"}, + "text": {"type": "mrkdwn", "text": chunk}, }) - break - # ── Pie ─────────────────────────────────────────────────────────────────── - blocks.append({ - "type": "context", - "elements": [{"type": "mrkdwn", - "text": f"{campaigns_analyzed} campañas analizadas ayer"}], - }) + # Botones de pausa por anuncio + camp_blocks.extend(_ad_action_blocks(ads)) - result = _post( - "chat.postMessage", - channel=config.SLACK_CHANNEL_ID, - blocks=blocks, - text=f"Meta Optimizer — {prefix} — {date_label}", - ) - return result.get("ts") if result.get("ok") else None + _post( + "chat.postMessage", + channel=config.SLACK_CHANNEL_ID, + blocks=camp_blocks, + text=name, + ) + + return ts def send_execution_summary(log: dict):