48 Commits

Author SHA1 Message Date
f1fba2dfb2 Show git commit hash as version in Slack header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 23:10:26 +02:00
4848253c49 Fix leadform double-attribution and Slack message truncation
- airtable_client: restrict leadform cursoid path (path 5) to PMX-only
  campaigns, preventing triple-attribution to search+pmx+leadform companion
- slack_reporter: derive margen from ingreso-coste instead of stale MetricasDiarias
  field; split monthly rankings from fields to separate section blocks; add
  _trunc() helper to clamp all text blocks under Slack's 3000-char limit
- agent: increase portfolio_daily_analysis max_tokens 500→800
- backfill: extend to June 11

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 23:04:40 +02:00
0eb360bf6d Increase decide() max_tokens to 600 to fix JSON truncation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 19:06:41 +02:00
170f0a3207 Add 5th lead attribution path: cursoid + Google-Ads-Notifications UserAgent
Leadform leads in Airtable now counted for PMX campaigns via
attr_cursoid + UserAgent='Google-Ads-Notifications', both for
monthly totals and daily MetricasDiarias.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 18:35:55 +02:00
c025a5f828 Downgrade Claude calls to sonnet-4-6 for cost efficiency
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 10:00:58 +02:00
3028123c81 Switch all Claude calls to claude-opus-4-8
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 09:54:05 +02:00
Francisco Monteagudo
bf945f2d75
Add SLACK_WEBHOOK_URL to environment variables 2026-06-05 10:27:18 +02:00
d82e604a15 Add attr_cursoid + attr_utm_source as 4th lead attribution path
For PMX/Search campaigns, match leads by course number and UTM source
when direct campaign ID attribution is missing. Fixes campaigns like
fco_pmx_436 (2→48 leads) and fco_pmx_1525 (0→84 leads).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 22:31:03 +02:00
b30a075c59 Fix leadform attribution: sum PMX + leadform conversions for margin analysis
- Add get_monthly_metrics_all() bulk method to GoogleAdsClient
- Use bulk fetch in first pass instead of N individual API calls
- Pre-compute leadform conversions per course number
- For PMX campaigns with leadform companion, leads_grupo = PMX conv + leadform conv
- fco_pmx_60 now correctly shows 334 leads at 61% margin (was negative)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 21:53:45 +02:00
a94c18c13c Add daily portfolio analysis and weekly strategic report
- agent.py: portfolio_daily_analysis() for daily Slack block,
  weekly_strategic_analysis() for deep weekly report
- run.py: call portfolio analysis before Slack send
- slack_reporter.py: add strategic diagnosis block at end of daily report
- weekly_report.py: standalone weekly report script
- .github/workflows/weekly.yml: runs Mondays at 9am (CEST)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 20:22:37 +02:00
aa9225d338 Add PPLMedio, CPAMedio, CosteMes, ConvMes to GAMes record
Recalculated each day from fco_ campaign totals:
- CosteMes / ConvMes: from Google Ads monthly accumulated data
- PPLMedio: weighted average by Airtable leads
- CPAMedio: CosteMes / ConvMes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 17:03:03 +02:00
a489a08785 Fix margin consistency: use MetricasDiarias for sumatorio cost
Both the monthly summary and the daily table now derive coste
from MetricasDiarias entries, so totals match exactly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 12:46:07 +02:00
624f5e484d Slack daily margin table: show only sumatorio margin
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:48:43 +02:00
bdc0d5ede3 Add GAMes table and daily margin table in Slack
- GAMes: new Airtable table aggregating daily fco_ metrics (coste, ingreso_sum, ingreso_lxp, leads, leads_lake)
- run.py: accumulate fco_ daily aggregate and write to GAMes each run
- slack_reporter.py: replace sparkline with daily margin % table (Sumatorio + LeadsxPPL per day)
- backfill_games_mayo.py: populated GAMes with all 17 existing May days

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:44:18 +02:00
523e04d61f Backfill Google Ads daily leads for May in MetricasDiarias
- Add get_daily_conversions_for_month() to GoogleAdsClient
- Add backfill_leads_google_mayo.py: populates 'leads' (Google conv) for all May days
- 147 day-entries updated across 49 GACampaignMes records

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 10:47:27 +02:00
f65f793c95 Rename leads field: Google Ads as 'leads', Airtable as 'leads_lake'
- MetricasDiarias now stores both: leads (Google conv) and leads_lake (Airtable count)
- Update backfill and migration scripts accordingly
- migrate_leads_field.py executed: 49 May records updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 10:36:20 +02:00
f1dec1c887 Add daily leads count per campaign to MetricasDiarias
- Fetch leads from Airtable per campaign on yesterday's date before second pass
- Include 'leads' field in MetricasDiarias JSON alongside coste/ingreso/margen
- Add get_leads_by_campaign_on_date() bulk fetch in AirtableClient
- Add backfill_leads_mayo.py one-off script (already executed for days 01-03)
- Fix UnboundLocalError: remove duplicate local timedelta import inside run()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 10:24:28 +02:00
26eeff794c Count Google Lead Form leads in Airtable lead attribution
Leads submitted via Google Lead Form arrive with UserAgent
'Google-Ads-Notifications' and campaign name in attr_referer.
Added this as a fourth attribution clause in get_leads_this_month_gads()
to reduce discrepancy between Airtable and Google Ads conversion counts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 12:11:22 +02:00
a85bc90b80 Fix Slack monthly summary: correct investment, conversions and margin calculations
- Use CosteMes and ConvMes from all previous month fco_ campaigns (not just
  those active in current month) to avoid missing paused campaigns
- Show day-1 cierre block with Google Ads data labeled explicitly
- Show current month as 0 on day 1 (no data yet)
- Fetch Campaign Name in get_metricas_diarias_prev_month for fco_ filtering
- Fetch prev month data for days 1-5 (last 5 days can span two months)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 10:55:58 +02:00
64fb151de9 Add GitHub Actions workflow for daily execution at 2 AM (CEST)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 13:40:58 +02:00
3eec871d93 Add Slack summary report with daily metrics and campaign rankings
- New slack_reporter.py: posts daily summary to Slack after each run
- Shows monthly investment, revenue (sumatorio + leadsxPPL) and margins
- Rankings of best/worst fco_ campaigns for last 5 days and current month
- Alerts for campaigns with losses > 200€ or margin < -50%
- MetricasDiarias uses yesterday's data (Google Ads lag)
- SLACK_WEBHOOK_URL added to config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 12:38:10 +02:00
45e9515ae8 Add daily metrics snapshot (MetricasDiarias) to GACampaignMes
Each optimizer run writes yesterday's cost, revenue (conversions x PPL)
and margin per campaign as a JSON entry keyed by day number into the
MetricasDiarias field. Yesterday is used because Google Ads data for
the current day is not yet available.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 10:44:53 +02:00
d58448f698 Add technical documentation (docu.md)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 15:19:30 +02:00
369bca604e Add leadform campaign detection and log notes for attribution discrepancies
- Detect courses with _leadform companion campaigns before first pass
- Add ℹ️ LEADFORM note in Log for campaigns that use Google's native lead form (leads bypass the website and don't reach Airtable)
- Add ⚠️ LEADFORM COMPANION note for sibling campaigns of the same course (explains conversion count discrepancy)
- Also moves _course_num and courses_with_both detection to pre-pass (before first loop) so PMX analysis uses correct leads_grupo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:32:38 +02:00
9edd74b9ad Add PMX attribution detection and ConvLeadsLakeMesGrupo field
- Detect Search+PMX companion campaigns by course number
- Add PMX attribution warning to Log field for Search campaigns with active PMX
- Add ConvLeadsLakeMesGrupo: for PMX with Search companion uses Search leads count

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 14:46:03 +02:00
a1aef8f58e Multiple fixes: lead matching, metrics aggregation, Log field, SPRINT with capping=0
- Fix GoogleCampaignID matching with comma delimiters (exact word boundary)
- Remove attr_utm_medium filter (campaign ID filter is specific enough)
- Fix get_campaign_metrics: accumulate all daily rows for monthly totals
- Fix SPRINT urgency: only trigger when capping > 0
- Add Log field to GACampaignMes with Airtable vs Google Ads discrepancy info
- batch_update_gacampaignmes_advice now also updates Log field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 11:05:06 +02:00
5639bd6e76 Fix ConvLeadsLakeMesFinal: use fresh leads count instead of stale pre-run value
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 13:08:52 +02:00
c62e2de9e8 Add FIND on GoogleCampaignID field as fallback for lead matching
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 12:49:16 +02:00
c728cfc149 Improve lead tracking and agent advice quality
- Use GACampaignID field + FIND(gad_campaignid) fallback in Leads Lake filter
- Add ConvLeadsLakeMes to campaign data for tracking discrepancy comparison
- Update ConvLeadsLakeMesFinal daily as snapshot of Airtable lead count
- Agent: ignore capping when capping=0 in advice

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 12:25:00 +02:00
e80ed58000 Fix leads filter: use FIND(gad_campaignid) in attr_referer instead of formula field; remove catalog Leads Lake update
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 10:23:08 +02:00
88523dc0a7 Simplify Leads Lake filter: remove utm_source, keep only GoogleCampaignID + utm_medium=paid_search 2026-04-21 16:10:53 +02:00
7bd1cf20da Add Criticidad field sync to GACampaignMes
- batch_update_gacampaignmes_advice() replaces consejos method, also saves Criticidad
- Criticidad mapped from priority: Crítico (PAUSAR/SPRINT), Peligro, Mantener
- Updated in same batch as Consejo after second pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 13:12:05 +02:00
d9a8e7ee0d Update campaign status in both tables from per-campaign Google Ads metrics
- get_active_gacampaignmes() now returns campaign_at_id (Google Ads Campaigns record ID)
- New batch_update_status() updates Status in GACampaignMes and Google Ads Campaigns
- run.py first pass collects status from get_campaign_metrics() and batch-updates
  both tables at end of pass, ensuring fresh status after each run

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 13:05:42 +02:00
f6f897adeb Include campaign status in analysis and fix agent reactivation advice
- analyzer.py: add status_google (ENABLED/PAUSED) to analysis dict
- agent.py: instruct agent never to recommend reactivating an ENABLED campaign;
  mention reactivation in consejo only when campaign is PAUSED

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 13:01:41 +02:00
a5ce85ed32 Add GACampaignMes sync and use it as optimization source
- New GACampaignMes table tracks monthly state of campaigns with activity
  (ENABLED status or cost > 0 this month)
- sync_gacampaignmes(): creates/updates records with PPL, CPAMax, CosteMes,
  ConvMes, CapTotalMes, Status for current month (numeric Mes: 1-12)
- get_active_gacampaignmes(): reads GACampaignMes as source for optimization loop
- get_leads_this_month_gads(): filters leads by GoogleCampaignID + utm_source
  (pmx/google) + utm_medium (paid_search) instead of attr_cursoid
- update_gacampaignmes_leads_lake(): links leads to GACampaignMes Leads Lake field
- batch_update_gacampaignmes_consejos(): saves agent advice to Consejo field
- Google Ads Campaigns table simplified to catalog only (name, status, PPL)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 13:00:34 +02:00
7344cc0c52 Order campaign output by priority and add direct Google Ads links
- Campaigns now shown in three sections:
  1. ACCIONES CRÍTICAS (urgencia PAUSAR/SPRINT)
  2. ACCIONES NO CRÍTICAS (other budget changes)
  3. SIN CAMBIOS (MANTENER)
- First pass collects all data, then sorts before printing
- Each campaign shows direct Google Ads link with account ID
- Summary table also grouped by section with links

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 18:27:51 +02:00
b0f0ad394b Recalculate PPL weighted by centro invalidation rate and sync CapTotalMes from CursoMes
- PPL = sum(PPL_centrocurso × % invalidación_centro) for ABIERTO records
  (% invalidación stored as decimal 0-1, not percentage)
- CapTotalMes synced from CursoMes.Caping Admitido for current month/year
  (0 if no record found in CursoMes)
- Table reference fixed: CENTROCURSO → CentroCurso
- Added CursoMes table client in AirtableClient.__init__

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 16:41:48 +02:00
2191a498d7 Link Leads Lake records to campaign on each run
- get_leads_this_month() now returns (count, [record_ids])
- update_campaign_leads_lake() updates the multipleRecordLinks
  field 'Leads Lake' in Google Ads Campaigns with the lead IDs
  found for that course this month
- run.py calls the update after fetching leads per campaign

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 10:21:30 +02:00
ec3802eccf Auto-calculate PPL and CPAMax from CENTROCURSO on sync
- Add build_ppl_lookup(): loads Cursos + CENTROCURSO ABIERTO in 2
  bulk calls, returns {cursoid_text: sum_ppl} for all courses
- sync_campaigns_from_google_ads() now accepts ppl_lookup and
  updates PPL = sum(CENTROCURSO.PPL where Estado ROI=ABIERTO)
  and CPAMax = PPL * 70% for each campaign
- run.py builds the lookup before sync and passes it in

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 12:35:49 +02:00
952fabe4ca Sync ConvMes and CosteMes to Airtable on each run
- Add get_monthly_metrics_all() in GoogleAdsClient: fetches
  conversions and cost for all campaigns in one query
- sync_campaigns_from_google_ads() now accepts monthly_metrics
  and updates ConvMes and CosteMes fields in Airtable
- run.py fetches monthly metrics before sync and passes them

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 21:12:05 +02:00
e4362c6ad9 Add sync detail and per-campaign advice
- Sync shows campaign ID, name and status for new campaigns,
  and field-level changes (before/after) for updated ones
- Agent now returns a 'consejo' field with a specific actionable
  recommendation per campaign
- Summary table includes leads, margin and consejo per campaign

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 16:16:43 +02:00
2f88eb119d Add execution logging to logs/ directory
Each run saves a timestamped log file (logs/YYYYMMDD_HHMMSS.log)
with full output. logs/ is gitignored.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 16:13:43 +02:00
f954b7ee38 Fix encoding, batch sync and field mapping
- Force UTF-8 stdout with line buffering to fix Windows encoding error
- Use batch_create/batch_update in Airtable sync (10x faster)
- Fix CampaignID field type (string, not int)
- Fix FechaEntrada → Fecha in leads query

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 16:10:56 +02:00
ef7d494aeb
Merge pull request #1 from jmgomezroi/claude/help-project-setup-t4hyw
Claude/help project setup t4hyw
2026-04-17 14:02:45 +02:00
Claude
67d5104dca
Sync campaign Status from Google Ads (ENABLED→Activa, PAUSED→Pausada)
Both on create and update, mirror the real Google Ads status instead
of defaulting to Pausada.

https://claude.ai/code/session_01WEcdWPxGWZKk8FGwBRLGR2
2026-04-17 11:52:38 +00:00
Claude
905ad3bcd2
Align field names with actual Airtable schema
- Campaigns: Status='Activa' filter (was Activa=TRUE), CPAMax (was
  CPAMaximo), add MargenObjetivo and CursoID Text per campaign
- Leads: filter by attr_cursoid + creado (was GoogleCampaignID + Fecha)
- analyzer: expose margen_objetivo and margen_ok per campaign
- config: remove global MARGEN_MINIMO, now per-campaign via Airtable
- sync: new campaigns created with Status='Pausada'

https://claude.ai/code/session_01WEcdWPxGWZKk8FGwBRLGR2
2026-04-17 11:48:52 +00:00
Claude
d68f9f5659
Sync Google Ads campaigns to Airtable before analysis
Before each run, fetch all non-removed campaigns from Google Ads
and upsert them into the Airtable campaigns table: new campaigns
are created (Activa=False, pending manual PPL/Cap/CPA setup) and
existing ones get their name updated if it changed.

https://claude.ai/code/session_01WEcdWPxGWZKk8FGwBRLGR2
2026-04-17 11:33:37 +00:00
f0d7f65c1a Initial commit 2026-04-17 12:53:25 +02:00