# Changelog of MachineryTrader Scraper — US Heavy Equipment Listings (`rastriq/machinerytrader-discovery`) Actor

- **URL**: https://apify.com/rastriq/machinerytrader-discovery/changelog.md
- **Full Actor documentation**: https://apify.com/rastriq/machinerytrader-discovery.md

## Changelog — MachineryTrader Scraper — US Heavy Equipment Listings

### 2026-10-04 — proxy-opt (build proxy-opt)

- O1/R1: PortManager con lease por worker (`acquire`/`release`): dos workers ya no comparten puerto/IP; cada unidad excluye los puertos ya probados. TTL de puerto quemado 5 -> 20 min (sticky Decodo = 60 min).
- O2/R2: deteccion de challenge Cloudflare/Turnstile ademas de Imperva (`challenge_kind`) en warmup y en paginas de listado. Un challenge sin cards quema el puerto y rota sesion (no se confunde con pagina vacia real, no marca la unidad como completa); si Turnstile se resuelve durante la espera se sigue normal.
- O3/R4: bloqueo de `usercentrics.eu` (CMP ~42 % de la pagina; el dato sale del SSR inline).
- O4: puertos usados por unidad (`ports_used`) y reuse de navegador mantiene su puerto/lease.
- O5: BREAKER por unidad: >= 26 kB de proxy por item con >= 5 navegaciones, o 3 challenges seguidos -> corta la unidad (no done; el checkpoint la reintenta).
- O6/R5: telemetria de produccion: contadores challenges/resolved/burned/ports_used por unidad y resumen `[proxy] warmups_total=...`. Muestras HTML crudas solo con `_proxy_debug` (admin).
- Warmup en `/robots.txt` por defecto (`_warmup_mode` robots|home|none, solo admin). Challenge en el warmup -> quema puerto y rota antes del listado (cuenta para burned_streak, no para el breaker).
- MEDICION (04-10, cranes/motor_graders/pipelayers, publico, maxPages 15): pagina limpia 233 -> 125 KB (-46 %), 8,5 -> 4,4 KB/item. A/B warmup: home 3/5 sesiones pasan CF (challenge en warmup predice challenge en listado 2/2), robots 4/4, none 4/5. Run 3 workers 4096 MB (XpbyaSeN7UHUDqNnD): puertos 10009/10006/10017 distintos, 42/42 paginas OK, 0 challenges, 1.174 items = mismos item_id y 54 claves identicas campo a campo que la fase 0; pipelayers 334/334 (TP 12, antes incompleta). Corte+resurrect (q1mjmXpPJI4Cvof7n): reanuda en p5, p1-p14 sin huecos, 392 items, 0 duplicados. Proyeccion 12.350 pags: ~1,6 GB limpio, ~2,3 GB central (5 % challenges).

### 2026-10-04 — telemetria de proxy (build proxy-test, solo medicion)

- Bytes reales por respuesta (`request.sizes()`, fallback `content-length`) agrupados por fase (warmup/page/other), 1p/3p, resource_type y host. Log `[proxy] unit= warmup_kb= page_kb_avg= items= kb_item=` por unidad y resumen + KV `PROXY_STATS` (KV por defecto del run). Muestras del HTML crudo de p1-p2 (max 12) en el KV por defecto. Sin cambios de logica, URLs, bloqueo ni output.

### 2026-10-04 — convencion tiempo_uso (sin push)

- Nuevas ext_tiempo_uso_unidad (h|km|mi|null) y ext_tiempo_uso_calidad (cero|horas_igual_km|unidad_sin_confirmar|null) en live y preloaded (derivadas idempotentes de lo almacenado, sin re-scrapear). Formato '6,811 h' intacto. Fix de etiqueta: millas ya no se etiquetan 'km' (texto 'mi' -> 'N mi', numero sin conversion; preloaded: 'km' almacenado en URL .com -> 'mi'); Mileage sin unidad ya no se etiqueta 'h'. hours_numeric english sin cambios. Declaradas en dataset_schema.json.

### 2026-10-04 — v2.15.0: cobertura completa .es (coverage plan) + fixes de URL

Medicion (investigacion 2026-10-04, solo lectura): universo .es sin accesorios 326.124 (equipo `For Retail|For Rent|For Lease|Upcoming Auctions` 284.703 + desguace `Condition=DISMANTLED` 41.421); capturable con 2.14 ~217K (~67 %). Prototipo de particion por facetas: 111 unidades, Σ LC = 326.124, max 9.996 por unidad. Las 3 cards "perdidas" de `plan_upc_p1.html` (25/28 aceptadas) apuntan a auctiontime.es.

- `_coverage_plan` (solo admin; ignorado con aviso si no es `_admin_snapshot`): `{"scope":"equipo|desguace","top_ids":[...],"part":[i,n],"dry_run":bool}`, solo `country: es`. Planifica por facetas (`getfacet`/`ajaxsearch` desde Camoufox con XSRF): una unidad por categoria de nivel 1; si pasa de 9.500 se parte por fabricante (bin-packing first-fit decreciente, <=60 nombres), un fabricante > tope por categoria hija y, si no cabe, por rangos de Year (el residual sin ano se loguea). Invariantes en codigo (suma hijos == padre; suma unidades + residual == total de la top); si fallan, `Actor.fail` antes de paginar.
- Plan persistido por top en el KV `machinerytrader-checkpoints` (`PLAN_{mes}_{pais}__{scope}__C{top}`): se reutiliza al reanudar para que los slugs de checkpoint no cambien. `dry_run` solo guarda `PLAN_*` si el snapshot empieza por `zz-`.
- Unidades `_planned`: URL = query exacta del plan + `sort=4`; no se auto-shardean (ni YEAR ni STATE); COVERAGE contra el LC del plan. `categoria`/`categoria_input_id` conservan el nombre/slug de la categoria top (compatible con preloaded).
- F1b (publico): attachments con scope 81 (crushers, augers, snow_blowers, welding_equipment, stump_grinders, asphalt_pavers) ahora llevan `ScopeCategoryIDs=81&` delante de `Category=`; antes la paginacion devolvia el catalogo sin filtrar. Ninguna otra URL cambia.
- F5 (publico): `is_product_url` acepta `auctiontime.` (subastas proximas ~11 % de AuctionTime); EXTRACT_JS usa como respaldo el primer enlace con id de 5+ digitos si la card no tiene enlace de titulo.
- F6 (aditivo): `ext_tipo_anuncio` = venta | alquiler | subasta | desguace segun la URL. Se marca, no se filtra. `renta_dia` sigue null.
- Smoke 04-10 (build cov-test): (1) la faceta Category filtrada por su propia categoria devuelve `SelectedOptionTree` + `FullOptionTree` (opciones duplicadas): solo se lee `FullOptionTree`. (2) En una busqueda que mezcla venta y alquiler el sitio etiqueta todo como "para la venta", asi que el alquiler pasa a un scope propio `alquiler` (`ListingType=For Rent|For Lease`, slugs `alq__`), que se lanza despues de `equipo` (`ListingType=For Retail|Upcoming Auctions`): lo ya capturado se salta por dedup y lo que solo esta en alquiler sale con URL `/en-alquiler/` -> `ext_tipo_anuncio=alquiler`.
- Opcion B (solo unidades `_planned` de equipo/alquiler; publico y desguace sin cambios): `categoria`/`categoria_input_id` = categoria conocida mas especifica del catalogo del actor, resuelta por el `category_text` de la card con un `_catmap` guardado en el plan (p. ej. Forklifts dentro de Lifts, Mini Excavators dentro de Excavators). Sin match: nombre de la unidad. No cambia la huella del plan.
- README e input schema: aviso de que `cat__trucks`, `cat__dump_trucks` y `cat__construction_attachments` devuelven el catalogo sin filtrar en .es y de que `cat__welding_equipment` (ID 1063) es "Zanjadora (accesorio)" en .es.

### 2026-10-03 — v2.14.4: checkpoint persistente + parada por duplicados segura

Medicion (fase 0, runs admin de septiembre, dataset machinerytrader-2026-09): de 418 unidades (categoria o shard), **146 terminaron por "2 consecutive full-dupe" antes de TotalPages**. Los duplicados venian de IDs que otros runs ya habian escrito (categorias solapadas, orden sort=4 agrupa los vistos). Hubo perdida probable en lifts por anio (~4-6K items estimados) y tipo C: trucks, dump_trucks, construction_attachments y dismantled devuelven el catalogo entero en los 27 shards (LC 278.530), cortados hoy en p2.

- Parada por duplicados: solo si TotalPages es conocido y se ha llegado a el. Sin TotalPages no para por racha; a las 20 paginas duplicadas seguidas corta SIN marcar la unidad completa. Coste estimado: +1.779 paginas (+17,5 %), ~+2 h de runtime y ~+$1,3-1,8; con la guarda tipo C ~+2,5K paginas (~+24 %) frente a +37.831 (+372 %) sin ella.
- Guarda de filtro inoperante: si un shard devuelve LC >= LC del padre, se pagina ese shard hasta TotalPages, se marcan done los shards restantes y el padre (evita multiplicar por 27).
- Checkpoint por unidad (rastriq_ckpt): clave CHECKPOINT\_{mes}\_{pais}\_\_{slug}, KV `machinerytrader-checkpoints` en admin. Se escribe tras cada push OK; las unidades done se saltan y las parciales reanudan en last_page+1. Sustituye al PROGRESS_CURSOR.
- IDs reservados justo antes del push y liberados si el push falla (o el run aborta): un push fallido ya no marca los IDs como vistos y la pagina se reintenta; con 3 workers y categorias solapadas 0 duplicados (test: 84/84) y maxListings exacto (test: 30 de 30, antes 86).
- Push fallido no quema el puerto del proxy; maximo 3 sesiones seguidas con push fallido por unidad, luego sale sin marcarla completa.
- 2 paginas vacias seguidas antes de TotalPages: se trata como bloqueo (reintento en otra sesion), no como fin (test: TP=12, p5-6 vacias -> antes done con p7-12 perdidas). TotalPages se arrastra entre sesiones de la unidad; sin TotalPages y sin ninguna card en la sesion tampoco es fin. Sin progreso, la unidad sale sin done y sin quemar puerto (el siguiente run reintenta; coste 2 paginas).
- SCRAPED_IDS (KV por defecto, migracion) no persiste IDs con push en vuelo.
- Pagina vacia dentro de TotalPages (o sin haber visto cards) ya no se salta: se reintenta en otra sesion (max 3 sin progreso, sin quemar puerto). Medicion: smoke 2026-10-04 pipelayers, p1 vacia transitoria -> antes hueco de 28 items marcado como hecho; ahora 0 huecos (test S1). Sin TotalPages, vacia seguida de cards = hueco -> reintento.
- Dataset admin por id (get_or_create) en vez de por nombre: tras borrar y recrear un dataset con el mismo nombre, el nombre apuntaba un momento al borrado ('Dataset was not found').
- Racha de 20 paginas duplicadas sin TotalPages acumulada entre sesiones (con pagesPerSession=10 antes llegaba a p62; ahora corta en p20).
- maxListings a mitad de pagina: la pagina cuenta como incompleta (checkpoint = pagina-1). `natural_end` ya no es True por maxListings ni por maxPages agotado.
- Categoria padre con shards: ya no se marca done antes de procesar los shards; solo cuando todos estan done.
- Carga de IDs existentes: solo campos item_id/ref_anuncio (antes el item completo).
- `_snapshot_dataset_name` (solo admin): dataset de prueba, claves de checkpoint con prefijo y sin publicar puntero. Se rechaza (error, sin escribir) si empieza por `machinerytrader-` o no empieza por `zz-` ni contiene el token `test`/`smoke`; el run termina FAILED (Actor.fail), no SUCCEEDED.
- Resumen final `[ckpt] RESUMEN n/m categorias INCOMPLETAS`.
- Contrato de salida sin cambios; fetchDetails sigue en false por defecto.

### 2026-09-18 — T6 push

- T6 (push 2026-09-18): mapper ftp28 whitelist 31 claves en modo live y preloaded (nueva funcion \_to_out_ftp28, independiente del default global "english"); input_schema.json prefill/default outputSchema -> "ftp28".

Formato: que cambio, por que, y **la medicion que lo respalda**. Un cambio sin numero detras es
una opinion; en este repo no cuenta como entrada de changelog.

Las versiones son las de Apify (`mayor.menor`). La plataforma no admite numero de parche.

### \[2.0] — estado a 21 de agosto de 2026

Version activa en Apify, con tag `latest`. Exportada a este repo el 21 de agosto de 2026.

No se reconstruye aqui el historial anterior: **inventarlo seria peor que no tenerlo**. Lo que hay documentado de ciclos previos vive en el runbook de la fuente, en `rastriq-docs/interno/runbooks/`. A partir de esta entrada, todo cambio se anota aqui con su medicion.

| | |
|---|---:|
| Ficheros fuente | 12 |
| Ejecuciones totales | 617 |
| Usuarios | 6 |
