---
name: monitor_freelance_projects_headless
kind: pipeline
lang: py
domain: pipelines
version: "1.0.0"
purity: impure
signature: "def monitor_freelance_projects_headless(category: str = 'it-programming', language: str = 'es', query: str = '', pages: int = 1, include_upwork: bool = False, upwork_query: str = 'custom software', duckdb_path: str = '', xlsx_path: str = '', port: int = 9334, profile_dir: str = '', timeout_s: float = 25.0) -> dict"
description: "Monitor de captacion de clientes freelance (Workana + Upwork -> DuckDB + Excel) en un Chrome headless AISLADO con perfil dedicado, lanzandolo y cerrandolo en cada corrida. Evita abrir pestanas en el navegador diario del usuario (chromium-personal, CDP 9222). Wrapper de monitor_freelance_projects que solo gestiona el ciclo de vida del navegador. Proyecto captacion_clientes."
tags: [market-intel, captacion_clientes, headless, cdp, freelance, scraper, recon]
uses_functions: [monitor_freelance_projects_py_pipelines]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/pipelines/monitor_freelance_projects_headless.py"
params:
  - name: category
    desc: "Categoria de Workana (?category=). Default 'it-programming'."
  - name: language
    desc: "Idioma de los proyectos de Workana (?language=). Default 'es'."
  - name: query
    desc: "Query libre aplicada a ambas fuentes (extra_query en Workana; sobrescribe upwork_query en Upwork si no esta vacia). Default vacio."
  - name: pages
    desc: "Numero de paginas de listado a recorrer por fuente. Default 1."
  - name: include_upwork
    desc: "Si True, scrapea Upwork ademas de Workana (tolerante a fallo). Default False (sus selectores no estan validados en vivo y requiere login)."
  - name: upwork_query
    desc: "Query para Upwork cuando include_upwork. Default 'custom software'. `query` lo sobrescribe si se pasa."
  - name: duckdb_path
    desc: "Ruta del archivo DuckDB de persistencia con dedup por url. Vacio -> ~/.fn_freelance/freelance.duckdb (se crea el directorio)."
  - name: xlsx_path
    desc: "Ruta del .xlsx de salida (hojas 'Nuevos' y 'Todos'). Vacio -> ~/.fn_freelance/freelance_projects.xlsx (se crea el directorio)."
  - name: port
    desc: "Puerto de remote-debugging del Chrome headless aislado que este wrapper lanza y al que apunta el monitor. Default 9334 (NO el 9222 del navegador diario)."
  - name: profile_dir
    desc: "user-data-dir dedicado del Chrome aislado. Vacio -> ~/.config/fn_scrape_chrome (se crea si no existe). Perfil persistente entre corridas."
  - name: timeout_s
    desc: "Timeout en segundos por pagina para los scrapers. Default 25.0."
output: "dict que SIEMPRE incluye {status: 'ok'|'error', port, profile_dir, launched: bool, closed: bool} y, en exito, las claves del resultado de monitor_freelance_projects (new_count, total_in_db, new_projects, xlsx_path, duckdb_path, sources). En error sin lanzar incluye `error`. El finally cierra siempre la instancia que lanzo (closed=True); si reutiliza un CDP ya vivo en el puerto, launched=False y closed=False (no cierra lo ajeno). Nunca lanza excepcion al caller."
---

## Ejemplo

```bash
# Monitor freelance en Chrome headless aislado (lanzar -> scrape -> cerrar).
# OJO: fn run pasa los args POSICIONALES, en el orden de la firma:
#   category, language, query, pages, ...
fn run monitor_freelance_projects_headless it-programming es "" 1
# -> {"status":"ok","port":9334,"profile_dir":"/home/<user>/.config/fn_scrape_chrome",
#     "launched":true,"closed":true,"new_count":N,"total_in_db":M,
#     "xlsx_path":"/home/<user>/.fn_freelance/freelance_projects.xlsx",
#     "duckdb_path":"/home/<user>/.fn_freelance/freelance.duckdb",
#     "sources":{"workana":{"count":N,"status":"ok"},"upwork":"skipped"}}
```

Invocacion directa del modulo (acepta flags `--category`/`--language`/`--pages`/...):

```bash
python/.venv/bin/python3 python/functions/pipelines/monitor_freelance_projects_headless.py \
    --category it-programming --language es --pages 2
```

## Cuando usarla

Usala para la ingesta diaria/programada (dag_engine) del monitor de captacion freelance del
proyecto captacion_clientes cuando NO quieras que el scraping abra pestanas en tu navegador
diario. Levanta su propio Chromium headless con perfil dedicado (puerto 9334) y lo cierra al
terminar — el navegador personal (`chromium-personal`, CDP 9222) queda intacto. Es el
reemplazo de llamar `monitor_freelance_projects` con `--port 9222` a pelo (que usaria el
navegador interactivo logueado).

## Gotchas

- **Impura: lanza y mata Chrome.** Arranca un Chromium headless via `systemd-run --user`
  (scope `fnscrape_dag_<port>`); si `systemd-run` no esta, cae a `subprocess.Popen` con grupo
  de proceso propio. Lanzarlo con `exec` directo desde el agente da **exit-144** — por eso
  systemd-run. En el `finally` siempre cierra lo que lanzo (`systemctl --user stop` del
  scope/service + respaldo `pkill -f "user-data-dir=<perfil>"`) y verifica con un GET final
  que el puerto ya no responde (`closed`).
- **Perfil dedicado persistente.** `~/.config/fn_scrape_chrome` sobrevive entre corridas
  (cookies/cache del scraping). No se borra. Borralo a mano si quieres sesion limpia.
- **Reutiliza CDP existente.** Si el puerto ya responde al arrancar, NO lanza otro Chrome:
  reutiliza el vivo y `launched=False` + `closed=False` (no cierra algo que no abrio).
- **Workana puede cambiar selectores o bloquear.** Workana es una SPA Vue: si cambia sus
  selectores o aplica anti-bot, el monitor devuelve `status: error` (sin inventar datos),
  pero el Chrome aislado **igual se cierra** en el finally. Upwork esta en `skipped` por
  defecto (selectores no validados en vivo + login).
