commit f4731fa87ed03c7b524b4841d02ce56623aa1223 Author: Alvaro Romero Date: Fri Jul 3 12:15:42 2026 -0400 feat: n8n initial commit — JavaScript (Node.js) n8n workflow automation + axios/cheerio/puppeteer/xlsx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f94721 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment / secrets +.env +.env.* +*.env + +# Build outputs +dist/ +build/ + +# Logs +*.log +scrape_log.txt + +# OS +.DS_Store +Thumbs.db + +# Temporary / backup files +*.bak +*.tmp +*_output.xlsx + +# Excel working files (may contain PII) +*.xlsx + +# Claude local settings (contains session info) +.claude/settings.local.json diff --git a/Automatización Bot Prisa yachts/PROYECTO.md b/Automatización Bot Prisa yachts/PROYECTO.md new file mode 100644 index 0000000..366af62 --- /dev/null +++ b/Automatización Bot Prisa yachts/PROYECTO.md @@ -0,0 +1,248 @@ +# Automatización Bot Prisa Yachts +**Fecha:** 2 julio 2026 +**Responsable:** Álvaro +**n8n:** https://n8n.crewinghunters.com + +--- + +## ¿Qué es esto? + +Sistema de automatización de captación de clientes para **Prisa Yachts LLC**, empresa de servicios náuticos en Florida. El proyecto tiene dos pilares: + +1. **Bot de WhatsApp** — atiende consultas 24/7, clasifica el servicio solicitado y notifica al especialista correcto +2. **Directorio marino de Florida** — base de datos de marinas, astilleros, almacenamiento y tiendas de suministros, con emails y websites, para campañas de captación + +--- + +## Empresa: Prisa Yachts LLC + +``` +📞 (786) 396-3315 +📧 info@prisayachts.com +🌐 prisayachts.com +📸 @prisayachts + +Slogan: "Safe Command ♦ Luxury Maintenance and Care" +``` + +### Servicios y responsables + +| # | Servicio | Especialista | WhatsApp | +|---|----------|-------------|----------| +| 1 | Engines & Mechanical | Álvaro | +1 (954) 655-4084 | +| 2 | Electrical & Electronics / NMEA | Álvaro | +1 (954) 655-4084 | +| 3 | Teak Deck Recovery | Federico | +1 (754) 209-3375 | +| 4 | Captaining & Crewing | Federico | +1 (754) 209-3375 | +| 5 | Yacht Care & Detailing | Federico | +1 (754) 209-3375 | +| 6 | Crew Placement & Staffing | Federico | +1 (754) 209-3375 | +| 7 | Other / Otro | Ambos | — | + +--- + +## Pilar 1 — Bot de WhatsApp + +### Arquitectura + +``` +Cliente WhatsApp + │ + ▼ +Meta Webhook ──► n8n Webhook (POST /prisa-whatsapp) + │ + ▼ + State Machine (Code node) + │ + ┌───────────┴───────────┐ + ▼ ▼ + Is Verify? Is Send to Client? + (GET challenge) │ + │ ▼ + Respond 200 Enviar mensaje al cliente + │ + Notify Specialist? + ┌────────┴────────┐ + ▼ ▼ + Is Both? Is Alvaro? + ┌──┴──┐ ┌──┴──┐ + ▼ ▼ ▼ ▼ + Álvaro Federico Álvaro Federico +``` + +### Flujo del bot (state machine) + +``` +PASO 1 — "new" / reset words (hola, hello, hi, menu, start) + → Enviar MENÚ PRINCIPAL (opciones 1-7) + → Estado: waiting_category + +PASO 2 — "waiting_category" + → Cliente responde 1-7 + → Enviar formulario del servicio específico + → Estado: collecting_info + +PASO 3 — "collecting_info" + → Cliente envía sus datos + → Enviar CONFIRMACIÓN al cliente + → Notificar especialista con resumen del caso + → Estado: complete + +PASO 4 — "complete" + → Siguiente mensaje reinicia el flujo desde el menú +``` + +### Estado actual del bot + +| Ítem | Estado | +|------|--------| +| Workflow creado en n8n | ✅ Desplegado | +| Credential WhatsApp_Prisa | ✅ Creada (placeholder) | +| Workflow activo | ✅ Activo | +| Webhook URL | `https://n8n.crewinghunters.com/webhook/prisa-whatsapp` | +| Token de Meta | ⏳ **Pendiente aprobación Meta** | +| Phone Number ID | ⏳ **Pendiente aprobación Meta** | + +### Pasos pendientes — Bot WhatsApp + +- [ ] **Meta aprueba** la cuenta de WhatsApp Business +- [ ] Actualizar credencial `WhatsApp_Prisa` en n8n: + - Cambiar `PLACEHOLDER_PENDING_META_APPROVAL` → `Bearer ` +- [ ] En los 3 nodos HTTP del workflow, cambiar URL: + - Reemplazar `PLACEHOLDER_PHONE_NUMBER_ID` → `` +- [ ] Configurar webhook en Meta Developer Portal: + - URL: `https://n8n.crewinghunters.com/webhook/prisa-whatsapp` + - Verify Token: (cualquier string, ej. `prisayachts2025`) + - Suscribir a: `messages` +- [ ] Prueba final: enviar "hola" al número del negocio desde WhatsApp + +--- + +## Pilar 2 — Directorio Marino de Florida + +### Objetivo + +Construir la base de datos más completa posible de negocios náuticos en Florida para usar en campañas de captación (email + WhatsApp). Cada registro incluye: + +`Nombre | Dirección | Teléfono | Email | Ciudad | Condado | Website` + +### Categorías del directorio + +| Hoja | Descripción | +|------|-------------| +| `MARINAS` | Puertos deportivos y marinas | +| `ASTILLEROS` | Boatyards y talleres náuticos | +| `ALMACENAMIENTO` | Boat storage / dry stack | +| `SUMINISTROS` | Tiendas de suministros marinos | + +### Cobertura geográfica actual + +El archivo principal es `Directorio_Marino_FL_Acumulativo_4.xlsx`. + +| Sesión | Zona | Condados principales | +|--------|------|---------------------| +| 1-3 | Florida Sur | Miami-Dade, Broward, Palm Beach | +| 4 | Florida Centro | Martin, St. Lucie, Okeechobee, Sarasota, Charlotte, Lee, Collier, Monroe (Keys) | +| 5 | Florida Oeste-Centro | Pinellas, Hillsborough, Manatee, Pasco, Hernando | +| 6 *(add_north_florida.js)* | Florida Norte | Volusia, Flagler, Indian River, Brevard, Citrus, Levy, Dixie, Taylor, Wakulla, Franklin, Gulf, Bay, Walton, Okaloosa, Santa Rosa, Escambia, Duval, St. Johns, Nassau | + +**Registros Florida Norte (Sesión 6):** +- Marinas: 128 +- Astilleros: 24 +- Almacenamiento: 16 +- Suministros: 37 +- **Total sesión 6: 205 registros** + +### Scraper de emails (`scrape_marine_emails.js`) + +Automatiza la búsqueda de emails y websites para los registros que no los tienen: + +``` +Para cada negocio en el Excel: + 1. Buscar en DuckDuckGo: "nombre" ciudad Florida marina contact email + 2. Filtrar dominios basura (Yelp, Google, Facebook, etc.) + 3. Scrapear homepage → página /contact → página /about + 4. Extraer emails con regex, puntuar por relevancia + 5. Guardar resultado en el Excel (checkpoint cada 25 filas) +``` + +**Última ejecución del scraper:** +- Procesó hoja MARINAS (174 filas) +- Guardado de emergencia a `_output.xlsx` (archivo bloqueado por Excel) +- Muchos registros de Miami/Broward sin email (negocios grandes, emails corporativos difíciles de scrapear) + +### Pasos pendientes — Directorio + +- [ ] Verificar completitud del Excel actual (contar registros por zona y categoria) +- [ ] Correr scraper en `MARINAS_NORTE`, `ASTILLEROS_NORTE`, `ALMACENAMIENTO_NORTE`, `SUMINISTROS_NORTE` +- [ ] Completar emails faltantes de Florida Sur manualmente (prioridad: marinas grandes) +- [ ] Decidir si expandir a otros estados (Georgia, Carolina del Sur, Texas...) +- [ ] Consolidar todos los acumulativos en un único archivo maestro limpio + +--- + +## Pilar 3 — Campañas de Captación (próximo paso) + +Una vez el directorio esté completo con emails, lanzar campañas automáticas: + +### Email marketing +- Plataforma a definir: Mailchimp / Brevo / n8n con SMTP +- Segmentación por tipo de negocio y zona geográfica +- Template con brochure de Prisa Yachts (ya existe `Prisa_Yachts_Brochure.pdf`) +- Medir aperturas y clicks + +### WhatsApp marketing (futuro) +- Usar la API aprobada de Meta para enviar mensajes masivos (templates aprobados) +- Segmentar por servicio relevante para cada tipo de marina +- Flujo: contacto frío → si responde, entra al bot automáticamente + +--- + +## Archivos del proyecto + +| Archivo | Descripción | +|---------|-------------| +| `create_whatsapp_workflow.js` | Despliega el bot en n8n (ejecutar una vez) | +| `scrape_marine_emails.js` | Scraper de emails del directorio | +| `add_north_florida.js` | Agrega datos de Florida Norte al Excel | +| `convert_to_pdf.js` | Genera `Prisa_Yachts_Brochure.pdf` desde HTML | +| `Directorio_Marino_FL_Acumulativo_4.xlsx` | Base de datos principal | +| `Directorio_Marino_FL_Acumulativo_4_output.xlsx` | Copia de emergencia del scraper | +| `Directorio_Marino_FL_Acumulativo_5.xlsx` | Datos adicionales (pendiente integrar) | +| `Prisa_Yachts_Brochure.html` | Brochure en HTML | +| `Prisa_Yachts_Brochure.pdf` | Brochure generado | +| `scrape_log.txt` | Log de la última ejecución del scraper | + +--- + +## Dependencias técnicas + +```json +{ + "axios": "^1.18.1", // HTTP requests + "cheerio": "^1.2.0", // HTML parsing / scraping + "puppeteer": "^25.1.0", // HTML → PDF + "xlsx": "^0.18.5" // Lectura/escritura Excel +} +``` + +**n8n:** Self-hosted en `https://n8n.crewinghunters.com` +**API Key n8n:** En `create_whatsapp_workflow.js` (hardcoded — mover a variable de entorno) + +--- + +## Resumen de prioridades + +### 🔴 Bloqueado (esperando tercero) +1. Aprobación de Meta para WhatsApp Business API + +### 🟡 En progreso +2. Completar directorio marino (scraper + revisión manual) +3. Integrar `Directorio_Marino_FL_Acumulativo_5.xlsx` al archivo principal + +### 🟢 Listo para ejecutar cuando Meta apruebe +4. Actualizar credenciales en n8n y probar bot en producción +5. Lanzar primera campaña de email a marinas de Florida Sur + +### 🔵 Próximos pasos estratégicos +6. Diseñar templates de WhatsApp aprobados por Meta +7. Construir workflow n8n para envío masivo de emails +8. Expandir directorio a otras costas (Gulf Coast, Carolinas) diff --git a/Automatización Bot Prisa yachts/Prisa_Yachts_Brochure.html b/Automatización Bot Prisa yachts/Prisa_Yachts_Brochure.html new file mode 100644 index 0000000..eee28ea --- /dev/null +++ b/Automatización Bot Prisa yachts/Prisa_Yachts_Brochure.html @@ -0,0 +1,498 @@ + + + + + + + +Prisa Yachts LLC - Professional Marine Services + + + + + + +
+
+
+ Prisa Yachts LLC +
+ +
+ +
+ +
+
Marine Excellence · Since 2025
+
+ Your vessel deserves
expert hands
every time. +
+

+ Prisa Yachts LLC delivers professional yacht care, certified captains, + full maintenance, and marine electrical & electronics services across + South Florida's waterways — from Miami to Palm Beach. +

+
+
+ + + + + + + + +
+

+ We are a South Florida-based marine services company specializing in yacht management, + professional crewing, preventive maintenance, and electrical & navigation systems. + Our certified team brings shipyard-grade quality directly to your vessel — at the dock, in the yard, or offshore. +

+
+ Service Area + Miami–Palm Beach + ICW · Offshore · Intracoastal +
+
+ + +
+ +
Our Services
+
+ +
+
+ + + + + + +
+
Yacht Care & Detailing
+

Complete vessel care programs tailored to your yacht's needs. We protect your investment with meticulous attention to every surface, system, and fitting.

+
    +
  • Full exterior wash & wax
  • +
  • Hull polishing & antifouling prep
  • +
  • Interior deep cleaning
  • +
  • Stainless & chrome brightwork
  • +
+
+ +
+
+ + + + + + + + + + + + +
+
Teak Deck Recovery
+

Full restoration of teak decks — from deep cleaning and caulking repair to full plank replacement. We bring weathered teak back to its original warmth and beauty.

+
    +
  • Deep cleaning & brightening
  • +
  • Caulk removal & re-seaming
  • +
  • Plank repair & full replacement
  • +
  • Sealing & long-term protection
  • +
+
+ +
+
+ + + + + + +
+
Professional Crewing
+

Certified captains and experienced crew for day trips, charters, vessel transfers, and ocean passages. Licensed, insured, and fully professional.

+
    +
  • USCG-licensed captains
  • +
  • Day & overnight charters
  • +
  • Vessel delivery & transfer
  • +
  • Offshore passage crew
  • +
+
+ +
+
+ + + + + + +
+
Maintenance & Repair
+

Scheduled preventive maintenance and on-call repair services for engines, mechanical systems, plumbing, and all onboard equipment.

+
    +
  • Engine servicing & oil changes
  • +
  • Bilge pump & through-hull service
  • +
  • Fuel system maintenance
  • +
  • Generator & AC service
  • +
+
+ +
+
+ + + + + + + +
+
Electrical & Electronics
+

Marine electrical systems, NMEA 2000 networks, navigation electronics, AIS, chartplotters, VHF, autopilot, and full wiring installations following ABYC standards.

+
    +
  • NMEA 2000 & 0183 integration
  • +
  • AIS transponder installation
  • +
  • Chartplotter & autopilot setup
  • +
  • DC/AC panel & shore power
  • +
+
+ +
+
+ + +
+ +
Built on the water.
Proven on the water.
+
+
+
25+
+
Years at Sea
+
Our team brings over 25 years of combined maritime experience — from commercial vessels to luxury yachts. We don’t just service boats; we know them from the inside out.
+
+
+
NMEA
+
Full Systems Knowledge
+
We understand the complete maritime ecosystem — propulsion, electrical, navigation, hull, and deck systems. Electronics installed following ABYC wiring standards.
+
+
+
24/7
+
WhatsApp Support
+
Direct line to our team via WhatsApp for urgent needs, scheduling, and real-time updates on your vessel — because boats don’t keep office hours.
+
+
+
+ + +
+ +
What Our Clients Say
+
+
+
★★★★★
+

"From day one, Prisa Yachts demonstrated extensive knowledge of the nautical world. They always communicated clearly, took care of every detail, followed safety best practices, and delivered on time and within budget."

+
Maria Cadenas · Yacht Owner
+
+
+
★★★★★
+

"Frederico and his deckhand were consummate professionals throughout. On time, in constant communication, and genuinely interested in making the experience as memorable as possible. I highly recommend their services."

+
Shawn M. · Charter Client
+
+
+
★★★★★
+

"Outstanding job on the teak deck cleaning! The wood looks amazing — even and well maintained. The team showed great attention to detail and professionalism throughout the entire process."

+
Brian Lopez · 60' Motor Yacht
+
+
+
★★★★★
+

"I've been looking for a captain with this level of professionalism for years. Frederico runs everything flawlessly — safety, service, and every detail on board. You can feel his passion for what he does."

+
Gary Mac · Recurring Client
+
+
+
+ + + + + + + + diff --git a/Automatización Bot Prisa yachts/Prisa_Yachts_Brochure.pdf b/Automatización Bot Prisa yachts/Prisa_Yachts_Brochure.pdf new file mode 100644 index 0000000..efffaf8 Binary files /dev/null and b/Automatización Bot Prisa yachts/Prisa_Yachts_Brochure.pdf differ diff --git a/Automatización Bot Prisa yachts/add_north_florida.js b/Automatización Bot Prisa yachts/add_north_florida.js new file mode 100644 index 0000000..bb57f13 --- /dev/null +++ b/Automatización Bot Prisa yachts/add_north_florida.js @@ -0,0 +1,307 @@ +// add_north_florida.js +// Agrega directorio marino de Florida Norte al Excel existente +const xlsx = require('xlsx'); +const path = require('path'); + +const EXCEL_PATH = path.resolve(__dirname, 'Directorio_Marino_FL_Acumulativo_4.xlsx'); + +// Encabezados de columnas +const HEADERS = ['#', 'NOMBRE', 'DIRECCION', 'TELEFONO', 'EMAIL', 'CIUDAD', 'CONDADO', 'WEBSITE']; + +// ══════════════════════════════════════════════════════════ +// MARINAS — FLORIDA NORTE +// ══════════════════════════════════════════════════════════ +const MARINAS = [ + // VOLUSIA + ['Halifax Harbor Marina','450 Basin St, Daytona Beach, FL 32114','(386) 671-3601','halifaxharbormarina@gmail.com','halifaxharbormarina.com','Daytona Beach','Volusia'], + ['Loggerhead Daytona Beach Marina','721 Ballough Rd, Daytona Beach, FL 32114','(386) 523-3100','daytona@equitylifestyle.com','loggerheadmarinas.com','Daytona Beach','Volusia'], + ['Coquina Marina Daytona Beach','841 Ballough Rd, Daytona Beach, FL 32114','(386) 317-0555','coquinamarinas@gmail.com','coquinamarinadaytona.com','Daytona Beach','Volusia'], + ['Halifax River Yacht Club','331 S Beach St, Daytona Beach, FL 32114','(386) 255-7459','hryc@hryc.com','hryc.com','Daytona Beach','Volusia'], + ['Daytona Marina & Boat Works','645 S Beach St, Daytona Beach, FL 32114','(386) 252-6421','dawn645@thedaytonamarina.com','thedaytonamarina.com','Daytona Beach','Volusia'], + ['Loggerhead Inlet Harbor Marina','133 Inlet Harbor Rd, Ponce Inlet, FL 32127','(386) 767-3266','','loggerheadmarinas.com','Ponce Inlet','Volusia'], + ['Sea Love Boat Works & Marina','4877 Front St, Ponce Inlet, FL 32127','(386) 761-5434','sealoveboatworks@gmail.com','sealoveboatworks.com','Ponce Inlet','Volusia'], + ['Lighthouse Boatyard & Marina','4958 S Peninsula Dr, Ponce Inlet, FL 32127','(386) 767-0683','','','Ponce Inlet','Volusia'], + ['Seven Seas Marina & Boatyard','3300 S Peninsula Dr, Port Orange, FL 32127','(386) 761-3221','sevenseasmarina@bellsouth.net','sevenseasmarina.com','Port Orange','Volusia'], + ['Adventure Yacht Harbor','3948 S Peninsula Dr, Port Orange, FL 32127','(386) 756-2180','','adventureyachtharbor.com','Port Orange','Volusia'], + ['New Smyrna Marina','200 Boatyard St, New Smyrna Beach, FL 32169','(386) 427-4514','','newsmyrnamarina.com','New Smyrna Beach','Volusia'], + ['City Marina New Smyrna Beach','201 N Riverside Dr, New Smyrna Beach, FL 32168','(386) 409-2042','','cityofnsb.com','New Smyrna Beach','Volusia'], + ['River Deck Marina','111 N Riverside Dr, New Smyrna Beach, FL 32168','(386) 428-7827','','riverdeckmarina.com','New Smyrna Beach','Volusia'], + ["Gerry's Marina",'179 N Causeway, New Smyrna Beach, FL 32169','(386) 428-2341','','gerrysmarina.net','New Smyrna Beach','Volusia'], + ['Smyrna Yacht Club','1201 S Riverside Dr, New Smyrna Beach, FL 32168','(386) 427-4040','','smyrnayachtclub.com','New Smyrna Beach','Volusia'], + ["Cameron's Marina",'2001 S Riverside Dr, Edgewater, FL 32132','(904) 428-3063','','','Edgewater','Volusia'], + // FLAGLER + ['Palm Coast Marina','15 Palm Coast Resort Blvd, Palm Coast, FL 32137','(386) 446-6370','','palmcoastmarina.net','Palm Coast','Flagler'], + ['Hammock Beach Marina','102 Yacht Harbor Dr, Palm Coast, FL 32137','(386) 348-3114','info@hammockbeachmarina.com','hammockbeachmarina.com','Palm Coast','Flagler'], + // INDIAN RIVER + ['Loggerhead Marina Vero Beach','1221 Marina Village Cir, Vero Beach, FL 32967','(772) 770-4470','','loggerheadmarinas.com','Vero Beach','Indian River'], + ['Vero Beach City Marina','3611 Rio Vista Blvd, Vero Beach, FL 32963','(772) 978-4960','','covb.org','Vero Beach','Indian River'], + ['Vero Marine Center','12 Royal Palm Pointe, Vero Beach, FL 32960','(772) 562-7922','','veromarine.com','Vero Beach','Indian River'], + ['Fins Marina','1660 Indian River Dr, Sebastian, FL 32958','(772) 589-4843','info@finsmarina.com','finsmarina.com','Sebastian','Indian River'], + ['Sebastian River Marina & Boatyard','8525 US Hwy 1, Sebastian, FL 32976','(772) 664-3029','d.hillman@sebastian-river-marina.com','marinaharborinmiccofl.com','Sebastian','Indian River'], + ['Sebastian Inlet Marina','8685 US-1, Micco, FL 32976','(772) 664-8500','SIMoffice@mywindward.com','sebastianinletmarina.com','Micco','Indian River'], + ['Treasure Coast Marina','5185 S US-1, Grant, FL 32949','','','','Grant','Indian River'], + ['Vero Beach Yacht Club','3601 Rio Vista Blvd, Vero Beach, FL 32963','','','','Vero Beach','Indian River'], + // BREVARD - NORTE + ['Titusville Municipal Marina','451 Marina Rd, Titusville, FL 32796','(321) 383-5600','tlawson@f3marina.com','titusvillemarina.com','Titusville','Brevard'], + ['Westland Boatyard and Marina','419 N Washington Ave, Titusville, FL 32796','(321) 267-1667','','westlandmarina.com','Titusville','Brevard'], + ['Kennedy Point Marina & Yacht Club','4749 S Washington Ave, Titusville, FL 32780','(321) 383-0280','','kennedypointmarina.com','Titusville','Brevard'], + // BREVARD - CENTRO + ['Cape Marina','800 Scallop Dr, Cape Canaveral, FL 32920','(321) 783-8410','dockmaster@capemarina.com','capemarina.com','Cape Canaveral','Brevard'], + ['Sunrise Marina','505 Glen Cheek Dr, Cape Canaveral, FL 32920','(321) 783-9535','','sunrisemarina.com','Cape Canaveral','Brevard'], + ['Ocean Club Marina at Port Canaveral','930 Mullet Rd, Cape Canaveral, FL 32920','(321) 783-9001','info@oceanclubmarina-pc.com','oceanclubmarina-pc.com','Cape Canaveral','Brevard'], + ['Harbortown Marina Canaveral','2700 Harbortown Dr, Merritt Island, FL 32952','(321) 453-0160','info@harbortownmarina.com','harbortownmarina.com','Merritt Island','Brevard'], + ['Marker 24 Marina','1360 S Banana River Dr, Merritt Island, FL 32952','(321) 453-7888','','marker24marina.com','Merritt Island','Brevard'], + ['Cape Crossing Resort & Marina','201 Ivory Coral Ln, Merritt Island, FL 32953','(321) 576-4967','info@capecrossing.com','capecrossing.com','Merritt Island','Brevard'], + ['Cocoa Village Marina','90 Delannoy Ave, Cocoa, FL 32922','(321) 632-5445','cvmarina@cfl.rr.com','cocoavillagemarina.com','Cocoa','Brevard'], + ['Island Time Marina','400 W Cocoa Beach Cswy, Cocoa Beach, FL 32931','(321) 613-4852','','islandtimemarina.com','Cocoa Beach','Brevard'], + // BREVARD - SUR + ['Anchorage Yacht Basin','96 E Eau Gallie Cswy, Melbourne, FL 32937','(321) 773-3620','','anchorageyachtbasin.com','Melbourne','Brevard'], + ['Melbourne Harbor Marina','2210 Front St #101, Melbourne, FL 32901','(321) 725-9054','info@melbourneharbor.com','melbourneharbor.com','Melbourne','Brevard'], + ['Palm Bay Marina','4350 Dixie Hwy NE, Palm Bay, FL 32905','(321) 723-0851','','palmbaymarina.com','Palm Bay','Brevard'], + // CITRUS + ['Twin Rivers Marina','2880 N Seabreeze Pt, Crystal River, FL 34429','(352) 795-3552','','twinriversmarina.com','Crystal River','Citrus'], + ["Pete's Pier Marina",'1 SW 1st Pl, Crystal River, FL 34429','(352) 795-3302','','petespier.com','Crystal River','Citrus'], + ['Ozello Keys Marina','2355 S Ripple Path, Crystal River, FL 34429','(352) 228-8095','','ozellokeysmarina.com','Crystal River','Citrus'], + ['Riverhaven Marina','5296 S Riverview Cir, Homosassa, FL 34448','(352) 628-5545','','riverhavenmarina.com','Homosassa','Citrus'], + ['Homosassa Springs Marina','10806 W Halls River Rd, Homosassa, FL 34448','(352) 628-7334','','homosassaspringsmarina.com','Homosassa','Citrus'], + // LEVY + ['Cedar Key Marina II','12780 State Route 24, Cedar Key, FL 32625','(352) 543-6148','','cedarkeymarina.net','Cedar Key','Levy'], + ['Cedar Key City Marina','Dock St, Cedar Key, FL 32625','(352) 543-5132','cityhall@cedarkeyfl.us','','Cedar Key','Levy'], + // DIXIE + ['Horseshoe Beach Marina','262 E 3rd St, Horseshoe Beach, FL 32648','(352) 498-5405','','horseshoebeachmarina.com','Horseshoe Beach','Dixie'], + // TAYLOR + ['Sea Hag Marina','322 S Riverside Dr, Steinhatchee, FL 32359','(352) 498-3008','','seahag.com','Steinhatchee','Taylor'], + ['Steinhatchee River Inn & Marina','1111 Riverside Dr SE, Steinhatchee, FL 32359','(352) 498-4049','','riverinnandmarina.com','Steinhatchee','Taylor'], + ['Steinhatchee River Haven Marina','1110 Riverside Drive, Steinhatchee, FL 32359','(352) 498-0709','riverhaven@bellsouth.net','riverhavenmarinaandmotel.com','Steinhatchee','Taylor'], + ['Shelter Cove Marina','412 S Riverside Dr, Steinhatchee, FL 32359','(352) 210-1781','','sheltercovemarina.net','Steinhatchee','Taylor'], + ['Good Times Motel and Marina','7022 SW Hwy 358, Steinhatchee, FL 32359','(352) 498-8088','','goodtimesmotelandmarina.com','Steinhatchee','Taylor'], + ['Steinhatchee Landing Resort','203 Ryland Circle, Steinhatchee, FL 32359','(352) 498-0696','','steinhatcheelanding.com','Steinhatchee','Taylor'], + ['Keaton Beach Marina & Motel','20650 Keaton Beach Dr, Perry, FL 32348','(850) 578-2897','','','Perry','Taylor'], + ['Steinhatchee Marina at Deadman Bay','104 First Ave SW, Steinhatchee, FL 32359','(352) 578-1900','','shrmarina.com','Steinhatchee','Taylor'], + // WAKULLA + ['Shell Point Harbor Marina','85 Harbour Point Dr, Crawfordville, FL 32327','(850) 544-5658','Accounting@shellpointharbormarina.com','shellpointharbormarina.com','Crawfordville','Wakulla'], + ['Shields Marina','95 Riverside Dr, St. Marks, FL 32355','(850) 925-6158','info@shieldsmarina.com','shieldsmarina.com','St. Marks','Wakulla'], + ['Rock Landing Marina','91 Rock Landing Rd, Panacea, FL 32346','(850) 984-5844','','rocklandingmarina.com','Panacea','Wakulla'], + // FRANKLIN + ['Scipio Creek Marina','301 Market St, Apalachicola, FL 32320','(850) 653-8030','','scipiocreekmarina.com','Apalachicola','Franklin'], + ['Water Street Hotel & Marina','329 Water St, Apalachicola, FL 32320','(850) 502-5134','','waterstreethotel.com','Apalachicola','Franklin'], + ['Battery Park Marina','1 Bay Avenue, Apalachicola, FL 32320','(850) 653-9319','','','Apalachicola','Franklin'], + ['Deep Water Marina','329 Water St, Apalachicola, FL 32320','(850) 653-8801','','','Apalachicola','Franklin'], + ['Rainbow Inn & Marina','123 Water Street, Apalachicola, FL 32320','(850) 653-8139','','','Apalachicola','Franklin'], + ['Sportsmans Lodge Marina','99 North Bayshore Dr, Eastpoint, FL 32328','(850) 670-8423','','','Eastpoint','Franklin'], + ['The Moorings of Carrabelle','1000 Avenue A North, Carrabelle, FL 32322','','','themooringsofcarrabelle.com','Carrabelle','Franklin'], + ['C-Quarters Marina','501 St James Ave Hwy 98, Carrabelle, FL 32322','(850) 697-8400','','c-quartersmarina.com','Carrabelle','Franklin'], + ["Johnson's Carrabelle Marina",'803 NW Avenue A, Carrabelle, FL 32322','(850) 510-4196','','carrabelle-marina.com','Carrabelle','Franklin'], + ['Carrabelle Boat Club','1570 Hwy 98 W, Carrabelle, FL 32322','(850) 697-5500','info@carrabelleboatclub.com','carrabelleboatclub.com','Carrabelle','Franklin'], + ["Pirate's Cove Carrabelle Marina",'275 Timber Island Rd, Carrabelle, FL 32322','(850) 756-8144','','','Carrabelle','Franklin'], + ['MS Dockside Marina & Boatyard','292 Graham Dr, Carrabelle, FL 32322','(850) 697-3337','msdocksidemarina@gmail.com','msdockside.com','Carrabelle','Franklin'], + // GULF + ['Point South Marina - Port St. Joe','340 Marina Drive, Port St Joe, FL 32456','(850) 460-9780','info.portstjoe@pointsouthmarina.com','pointsouthmarinaportstjoe.com','Port St. Joe','Gulf'], + ["Captain's Cove Marina",'1646 Captain Carl Raffield Way, Port St Joe, FL 32456','(850) 227-3357','captaincove@yahoo.com','captainscovefl.com','Port St. Joe','Gulf'], + // BAY + ['Point South Marina - Bay Point','3824 Hatteras Ln, Panama City Beach, FL 32408','(850) 235-6911','BayPointMarina@StJoe.com','pointsouthmarinabaypoint.com','Panama City Beach','Bay'], + ['Bayside Marina','6325 Big Daddy Drive, Panama City Beach, FL 32407','(850) 234-7650','','bluegatemarinas.com','Panama City Beach','Bay'], + ['MarineMax Panama City Beach','3605 Thomas Dr, Panama City Beach, FL 32408','(850) 708-1317','','marinemax.com','Panama City Beach','Bay'], + ["Capt. Anderson's Marina",'5550 N Lagoon Dr, Panama City, FL 32408','(850) 234-3435','captandersonsmktg@aol.com','captandersonsmarina.com','Panama City','Bay'], + ['Treasure Island Marina','3605 Thomas Dr, Panama City Beach, FL 32408','(850) 234-6533','','treasureislandmarina.net','Panama City Beach','Bay'], + ['St. Andrews Marina','3151 W 10th St, Panama City, FL 32401','(850) 872-7240','','','Panama City','Bay'], + ["Pirate's Cove Marina PCB",'3901 Thomas Dr, Panama City Beach, FL 32408','(850) 234-3939','','piratescovemarinapcb.com','Panama City Beach','Bay'], + // WALTON + ["Tom's Landing Marina",'78 Ricker Ave, Santa Rosa Beach, FL','(850) 496-2741','tomslanding@gmail.com','tomslandingsrb.com','Santa Rosa Beach','Walton'], + ['Shipyard Marina Freeport','116 Shipyard Rd, Freeport, FL','(850) 880-2324','info@shipyardmarinafreeport.com','shipyardmarinafreeport.com','Freeport','Walton'], + // OKALOOSA + ['HarborWalk Marina','66 Harbor Blvd, Destin, FL','(850) 650-2400','','harborwalkmarina.net','Destin','Okaloosa'], + ['Fort Walton Beach Yacht Basin','104 Miracle Strip Pkwy SW, Fort Walton Beach, FL','(850) 244-5725','info@fwbyachtbasin.com','fwbyachtbasin.com','Fort Walton Beach','Okaloosa'], + ['Adventure Marina','1201-B Miracle Strip Pkwy SE, Fort Walton Beach, FL','(850) 581-2628','','adventuremarina.com','Fort Walton Beach','Okaloosa'], + ['Sunset Isle Yacht Club','1350 Miracle Strip Pkwy SE, Fort Walton Beach, FL','(850) 500-2628','','sunsetisle.com','Fort Walton Beach','Okaloosa'], + // SANTA ROSA + ['Santa Rosa Yacht & Boat Club','300 Pensacola Beach Rd, Gulf Breeze, FL','(850) 934-1005','','santarosayachtclub.com','Gulf Breeze','Santa Rosa'], + ['Milton Marina','Quinn Street, Milton, FL 32570','(850) 983-5466','parksandrec@miltonfl.org','miltonfl.org','Milton','Santa Rosa'], + ['Marquis Bayou Marina','7001 Old Spanish Trail, Milton, FL 32583','(850) 266-7728','','marquisbayou.com','Milton','Santa Rosa'], + ['East River Smokehouse Marina','8491 Navarre Pkwy, Navarre, FL 32566','(850) 939-2802','','eastriversmokehouse.com','Navarre','Santa Rosa'], + ['Bluewater Bay Marina','290 Yacht Club Drive, Niceville, FL 32578','','','bluewaterbaymarina.com','Niceville','Santa Rosa'], + // ESCAMBIA + ['Southwind Marina','10121 Sinton Dr, Pensacola, FL 32502','(850) 619-6348','gmsouthwind@gmail.com','southwindmarina.com','Pensacola','Escambia'], + ['Palafox Pier Yacht Harbor Marina','997 S Palafox St, Pensacola, FL 32502','(850) 432-9620','','','Pensacola','Escambia'], + ['Day Break Marina','811 South R Street, Pensacola, FL 32502','(850) 434-9022','','daybreakmarina.com','Pensacola','Escambia'], + ['Chico Marina','3009 Barrancas Ave, Pensacola, FL 32507','(850) 453-8863','','','Pensacola','Escambia'], + ["Mac's Marina",'31 Newman Avenue, Pensacola, FL 32507','(850) 453-3775','','macsmarinafl.com','Pensacola','Escambia'], + ['MarineMax Pensacola','1901 Cypress St, Pensacola, FL 32502','(850) 477-1112','','marinemax.com','Pensacola','Escambia'], + ['Island Cove Marina','806 Lakewood Rd, Pensacola, FL 32507','(850) 455-4552','office@segersmarine.com','segersmarine.com','Pensacola','Escambia'], + ['Rod & Reel Marina','10045 Sinton Dr, Pensacola, FL 32502','(850) 492-0100','','','Pensacola','Escambia'], + ['Pensacola Beach Marina','655 Pensacola Beach Blvd, Pensacola Beach, FL 32561','(850) 932-8466','','pensacolabeachmarina.com','Pensacola Beach','Escambia'], + ['Sabine Marina','715 Pensacola Beach Blvd, Pensacola Beach, FL 32561','(850) 932-1904','','sabinemarina.com','Pensacola Beach','Escambia'], + ['Pier One Marina','655 Pensacola Beach Blvd, Pensacola Beach, FL 32561','(850) 781-4180','','pieronemarina.com','Pensacola Beach','Escambia'], + ['Holiday Harbor Marina','14050 Canal-A-Way, Pensacola, FL 32507','(850) 492-0555','office@myholidayharbor.com','myholidayharbor.com','Pensacola','Escambia'], + // DUVAL + ['Port 32 Jacksonville','4234 Lakeside Dr, Jacksonville, FL 32210','(904) 387-5538','','port32marinas.com','Jacksonville','Duval'], + ['Ortega River Marina','4585 Lakeside Dr, Jacksonville, FL 32210','(904) 389-1199','ortegarivermarina@gmail.com','ortegarivermarinajax.com','Jacksonville','Duval'], + ['Sadler Point Marina','4599 Lakeside Dr, Jacksonville, FL 32210','(904) 384-1383','brooks@sadlerpoint.com','sadlerpointmarina.com','Jacksonville','Duval'], + ['Lakeshore Marine Center','3326 Lakeshore Blvd, Jacksonville, FL 32210','(904) 384-6447','info@lakeshoremarinecenter.com','lakeshoremarinecenter.com','Jacksonville','Duval'], + ['Arlington Marina','5137 Arlington Rd, Jacksonville, FL 32211','(904) 743-2628','info@arlingtonmarina.com','arlingtonmarina.com','Jacksonville','Duval'], + ['Seafarers Marina','455 Trout River Dr, Jacksonville, FL 32208','(904) 765-8152','seafarersmarina@aol.com','seafarersmarina.com','Jacksonville','Duval'], + ['HarborTown Marina Jacksonville','13846 Atlantic Blvd, Jacksonville, FL 32225','(904) 220-3600','','jaxharbortownmarina.com','Jacksonville','Duval'], + ['Julington Creek Marina','12807 San Jose Blvd, Jacksonville, FL 32223','(904) 268-5117','julingtoncreekmarina@outlook.com','marinaatjulingtoncreek.com','Jacksonville','Duval'], + ['Palm Cove Marina','14603 Beach Blvd Ste 100, Jacksonville, FL 32250','(904) 223-4757','','palmcovemarina.com','Jacksonville','Duval'], + ['Fort George Island Marina','9954 Heckscher Dr, Jacksonville, FL 32226','(904) 251-0050','','fortgeorgeislandmarina.com','Jacksonville','Duval'], + ["Lamb's Yacht Center",'3376 Lake Shore Blvd, Jacksonville, FL 32210','(904) 327-2285','','lambsyachtcenter.com','Jacksonville','Duval'], + ['Beach Marine','2315 Beach Blvd, Jacksonville Beach, FL 32250','(904) 249-8200','','jaxbeachmarine.com','Jacksonville Beach','Duval'], + ['Morningstar Marinas Mayport','4852 Ocean St, Atlantic Beach, FL 32233','(904) 236-6210','','morningstarmarinas.com','Atlantic Beach','Duval'], + // ST. JOHNS + ['St. Augustine Municipal Marina','111 Avenida Menendez, St. Augustine, FL 32084','(904) 825-1026','marina@citystaug.com','citystaug.com','St. Augustine','St. Johns'], + ['Camachee Cove Yacht Harbor','3070 Harbor Dr, St. Augustine, FL 32084','(904) 829-5676','CCYHoffice@MyWindward.com','camacheeisland.com','St. Augustine','St. Johns'], + ['The Conch House Marina Resort','57 Comares Ave, St. Augustine, FL 32080','(904) 824-4347','info@conchhousemarinaresort.com','conchhousemarinaresort.com','St. Augustine','St. Johns'], + ['English Landing Marina','509 S Ponce de Leon Blvd, St. Augustine, FL 32084','(904) 669-7363','info@englishlandingmarina.com','englishlandingmarina.com','St. Augustine','St. Johns'], + ["Cat's Paw Marina",'220 Nix Boat Yard Rd, St. Augustine, FL 32084','(904) 829-8040','Info@CatsPawMarina.com','catspawmarina.com','St. Augustine','St. Johns'], + ['Oasis Boatyard & Marina','256 Riberia St, St. Augustine, FL 32084','(904) 824-2520','','oasisboatyardandmarina.com','St. Augustine','St. Johns'], + // NASSAU + ['Fernandina Harbor Marina','3 S Front St, Fernandina Beach, FL 32034','(904) 310-3300','fernandinaharbor@alliancemarine.co','fernandinaharbormarina.com','Fernandina Beach','Nassau'], + ['Amelia Island Marina','251 Creekside Dr, Fernandina Beach, FL 32034','(904) 277-4615','ameliaisland@suntex.com','ameliaislandmarina.com','Fernandina Beach','Nassau'], + ['Olde Towne Marina','1420 N 14th St, Fernandina Beach, FL 32034','(904) 277-8511','oldetownemarina@bellsouth.net','oldetownemarina.com','Fernandina Beach','Nassau'], +]; + +// ══════════════════════════════════════════════════════════ +// ASTILLEROS — FLORIDA NORTE +// ══════════════════════════════════════════════════════════ +const ASTILLEROS = [ + ['Daytona Marina & Boat Works','645 S Beach St, Daytona Beach, FL 32114','(386) 252-6421','dawn645@thedaytonamarina.com','thedaytonamarina.com','Daytona Beach','Volusia'], + ['Sea Love Boat Works','4877 Front St, Ponce Inlet, FL 32127','(386) 761-5434','sealoveboatworks@gmail.com','sealoveboatworks.com','Ponce Inlet','Volusia'], + ['Lighthouse Boatyard','4958 S Peninsula Dr, Ponce Inlet, FL 32127','(386) 767-0683','','','Ponce Inlet','Volusia'], + ['Seven Seas Marina & Boatyard','3300 S Peninsula Dr, Port Orange, FL 32127','(386) 761-3221','sevenseasmarina@bellsouth.net','sevenseasmarina.com','Port Orange','Volusia'], + ['Westland Boatyard and Marina','419 N Washington Ave, Titusville, FL 32796','(321) 267-1667','','westlandmarina.com','Titusville','Brevard'], + ['Port Canaveral Marine','960 Mullet Rd, Cape Canaveral, FL 32920','(321) 799-9444','','portcanaveralmarine.com','Cape Canaveral','Brevard'], + ['Anchorage Yacht Basin','96 E Eau Gallie Cswy, Melbourne, FL 32937','(321) 773-3620','','anchorageyachtbasin.com','Melbourne','Brevard'], + ['Sebastian River Marina & Boatyard','8525 US Hwy 1, Sebastian, FL 32976','(772) 664-3029','d.hillman@sebastian-river-marina.com','','Sebastian','Indian River'], + ['Crystal River Marine Inc.','990 N Suncoast Blvd, Crystal River, FL 34429','(352) 795-2597','','crystalrivermarine.com','Crystal River','Citrus'], + ['Three Rivers Marine','1038 N Suncoast Blvd, Crystal River, FL 34429','(352) 563-5510','sales@threeriversmarineinc.com','threeriversmarineinc.com','Crystal River','Citrus'], + ['MS Dockside Marina & Boatyard','292 Graham Dr, Carrabelle, FL 32322','(850) 697-3337','msdocksidemarina@gmail.com','msdockside.com','Carrabelle','Franklin'], + ['Eastern Shipbuilding Group','432 Howard Road, Port St. Joe, FL 32456','(850) 763-1900','','easternshipbuilding.com','Port St. Joe','Gulf'], + ['Bay County Boatyard','101 N Church Ave, Panama City, FL 32401','(850) 215-9283','','baycountyboatyard.com','Panama City','Bay'], + ['Panhandle Marina & Boatyard','101 N Church Ave, Panama City, FL 32401','(850) 481-0556','info@panboatyard.com','panboatyard.com','Panama City','Bay'], + ['Marker 21 Boatyard & Marina','36 Miracle Strip Pkwy SW, Fort Walton Beach, FL','(850) 244-2722','markertwentyonemarina@gmail.com','marker21boatyardandmarina.com','Fort Walton Beach','Okaloosa'], + ['Coastal Marine Works','2816 Gulf Breeze Pkwy, Gulf Breeze, FL','(850) 637-9585','','coastalmarineworks.com','Gulf Breeze','Santa Rosa'], + ['Pensacola Marine Center','700 S Myrick St, Pensacola, FL 32501','(850) 439-1451','bob@pensacolamarinecenter.com','pensacolamarinecenter.com','Pensacola','Escambia'], + ["Pelican's Perch Marina and Boatyard",'40 Audusson Ave, Pensacola, FL 32507','(850) 453-3471','','pelicansperchmarina.com','Pensacola','Escambia'], + ['Huckins Yacht Corporation','3482 Lake Shore Blvd, Jacksonville, FL 32210','(904) 389-1125','service@huckinsyacht.com','huckinsyacht.com','Jacksonville','Duval'], + ['Sadler Point Marine Center','4599 Lakeside Dr, Jacksonville, FL 32210','(904) 384-1383','brooks@sadlerpoint.com','sadlerpointmarina.com','Jacksonville','Duval'], + ['Atlantic Coast Marine','13748 Atlantic Blvd, Jacksonville, FL 32225','(904) 221-0793','','atlanticcoastmarine.com','Jacksonville','Duval'], + ['St. Augustine Marine Center','404 S Riberia St, St. Augustine, FL 32084','(904) 824-4394','','staugustinemarine.com','St. Augustine','St. Johns'], + ['St. Augustine Shipyard','117 Dockside Dr, St. Augustine, FL 32084','','','camacheeyachtyard.com','St. Augustine','St. Johns'], + ['Tiger Point Marina & Boat Works','997 Egans Creek Ln, Fernandina Beach, FL 32034','(904) 277-2720','tigerpointmarina@comcast.net','tigerpointmarina.biz','Fernandina Beach','Nassau'], +]; + +// ══════════════════════════════════════════════════════════ +// ALMACENAMIENTO — FLORIDA NORTE +// ══════════════════════════════════════════════════════════ +const ALMACENAMIENTO = [ + ['All Aboard Storage - Ormond Beach','305 W Granada Blvd, Ormond Beach, FL','(386) 333-6801','','allaboardstorage.com','Ormond Beach','Volusia'], + ['Stor-It Boat & RV - Ormond Beach','99 Portland St, Ormond Beach, FL','(386) 676-5018','','storitboatandrv.com','Ormond Beach','Volusia'], + ['Barracuda Boat & RV Storage','412 W Park Ave, Edgewater, FL','(386) 847-8112','info@boatrvstorage.com','boatrvstorage.com','Edgewater','Volusia'], + ['RecNation RV & Boat Storage - Edgewater','403 Timaquan Trail, Edgewater, FL','(386) 433-8698','','recnationstorage.com','Edgewater','Volusia'], + ['All Aboard Storage - Daytona Beach','1325 S Nova Road, Daytona Beach, FL','(386) 200-9442','','allaboardstorage.com','Daytona Beach','Volusia'], + ['All Aboard Storage - Palm Coast','6372 SR 100 East, Palm Coast, FL','(386) 388-7739','','allaboardstorage.com','Palm Coast','Flagler'], + ['Stor-It of Flagler County','3700 E Moody Blvd, Bunnell, FL','(386) 263-3067','','storitofflaglercounty.com','Bunnell','Flagler'], + ['Sebastian Inlet Marina Dry Storage','8685 US-1, Micco, FL 32976','(772) 664-8500','SIMoffice@mywindward.com','sebastianinletmarina.com','Micco','Indian River'], + ['Mr. Stor-It Merritt Island','500 Cone Rd, Merritt Island, FL 32952','(321) 453-3400','','mrstorit.com','Merritt Island','Brevard'], + ['Crystal River Boat and RV Storage','S Suncoast Blvd, Crystal River, FL','','','crystalriverboatrvstorage.com','Crystal River','Citrus'], + ['Wakulla Station RV & Boat Storage','Crawfordville, FL','','','wakullastationrvandboatstorage.com','Crawfordville','Wakulla'], + ['Storage by the Sea','Navarre, FL','(850) 374-1648','','','Navarre','Santa Rosa'], + ['East Bay Boat & RV Storage','Navarre, FL','(850) 939-9040','','','Navarre','Santa Rosa'], + ['Old River Boat Storage','4837 Ocean St, Atlantic Beach, FL 32233','(904) 874-4132','oldriverboatstorage@gmail.com','oldriverboatstorage.com','Atlantic Beach','Duval'], + ["Solomon's RV & Boat Storage",'14255 Beach Blvd, Jacksonville, FL 32250','(904) 223-0888','','solomons.net','Jacksonville','Duval'], + ["Captain's Cove Dry Stack Storage",'1646 Captain Carl Raffield Way, Port St Joe, FL','(850) 227-3357','info@captainscovefl.com','captainscovefl.com','Port St. Joe','Gulf'], +]; + +// ══════════════════════════════════════════════════════════ +// SUMINISTROS — FLORIDA NORTE +// ══════════════════════════════════════════════════════════ +const SUMINISTROS = [ + ['West Marine - Daytona Beach','1300 W International Speedway Blvd, Daytona Beach, FL','(386) 255-2013','','westmarine.com','Daytona Beach','Volusia'], + ['Aloha Marine Center','1700 N Nova Rd, Holly Hill, FL','(386) 255-2345','','alohamarine.com','Holly Hill','Volusia'], + ['Bluewater Marine Daytona','730 Ridgewood Ave, Holly Hill, FL','(386) 255-7790','','bluewatermarine.com','Holly Hill','Volusia'], + ['Atlantic Marine Port Orange','520 Dunlawton Ave, Port Orange, FL','(386) 788-1644','robert@atlanticmarinefl.com','atlanticmarinefl.com','Port Orange','Volusia'], + ['North Causeway Marine','4 N Causeway, New Smyrna Beach, FL','(386) 427-5267','','northcausewaymarine.com','New Smyrna Beach','Volusia'], + ['West Marine Palm Coast','250 Palm Coast Pkwy NE, Palm Coast, FL','(386) 276-7001','','westmarine.com','Palm Coast','Flagler'], + ['Carp Coastal Marine & RV','4550 S US Hwy 1, Grant, FL','(321) 952-1303','carpcoastalmarine@carpindustries.com','carpcoastalmarine.com','Grant','Indian River'], + ['Vero Marine Center','12 Royal Palm Pointe, Vero Beach, FL','(772) 562-7922','','veromarine.com','Vero Beach','Indian River'], + ['West Marine West Melbourne','1001 W New Haven Ave, West Melbourne, FL','(321) 837-1113','','westmarine.com','West Melbourne','Brevard'], + ['MarineMax Cocoa','1410 King St, Cocoa, FL 32922','(321) 636-3142','','marinemax.com','Cocoa','Brevard'], + ['Boaters Exchange','2145 US-1, Rockledge, FL','(321) 638-0090','','boatersexchange.com','Rockledge','Brevard'], + ['West Marine Crystal River','160 SE Hwy 19, Crystal River, FL','(352) 563-0003','','westmarine.com','Crystal River','Citrus'], + ["Nobles' Marine Crystal River",'1931 NW Highway 19, Crystal River, FL','(352) 329-3739','','noblesmarine.com','Crystal River','Citrus'], + ['Homosassa Marine','3120 S Suncoast Blvd, Homosassa, FL 34448','(352) 628-2991','','homosassamarine.com','Homosassa','Citrus'], + ['Marina Hardware at Cedar Key','409 1st St, Cedar Key, FL','(352) 543-5804','','marinahardwarecedarkey.com','Cedar Key','Levy'], + ['RMS Marine Supply','3026 Coastal Hwy, Crawfordville, FL','(850) 926-3114','','rmsmarinesupply.com','Crawfordville','Wakulla'], + ['West Marine Panama City','427 E 23rd St, Panama City, FL','(850) 763-6104','','westmarine.com','Panama City','Bay'], + ['West Marine Panama City Beach','2225 Thomas Dr, Panama City Beach, FL','(850) 234-7699','','westmarine.com','Panama City Beach','Bay'], + ['Miller Marine Inc','7141 Grassy Point Rd, Panama City, FL','(850) 265-6768','','millermarineinc.net','Panama City','Bay'], + ["Howell Marine & Tackle Supply",'3100 W Highway 98, Panama City, FL','(850) 785-8548','','howelltacklesupply.com','Panama City','Bay'], + ['West Marine Destin','862-B Hwy 98 East, Destin, FL','','','westmarine.com','Destin','Okaloosa'], + ['The Ships Chandler - Destin','646 Harbor Blvd, Destin, FL','(850) 837-2262','','theshipschandler.com','Destin','Okaloosa'], + ['The Ships Chandler - Freeport','17309 Highway 331, Freeport, FL','(850) 880-6259','','theshipschandler.com','Freeport','Walton'], + ['Emerald Coast Marine Group','115 W John Sims Pkwy, Niceville, FL','(850) 389-8318','','emeraldcoastmarine.com','Niceville','Okaloosa'], + ['Gulf Breeze Marine','2933 Gulf Breeze Pkwy, Gulf Breeze, FL','(850) 932-1556','gbmarineshop@outlook.com','gulfbreezemarine.com','Gulf Breeze','Santa Rosa'], + ['West Marine Pensacola','5303 N Davis Hwy, Pensacola, FL','(850) 483-5523','','westmarine.com','Pensacola','Escambia'], + ['BGS Marine Sales & Service','3910 W Navy Blvd, Pensacola, FL','(850) 438-5934','sales@bgsmarinesales.com','bgsmarinesales.com','Pensacola','Escambia'], + ['Perdido Marine Supply','12490 Gulf Beach Hwy, Pensacola, FL','(850) 361-1191','','perdidotradingcompany.com','Pensacola','Escambia'], + ['Gulf Coast Marine Supply','7885 Pensacola Blvd, Pensacola, FL','(850) 476-4910','','gulfcoastmarine.com','Pensacola','Escambia'], + ['West Marine Jacksonville','4874 Big Island Dr, Jacksonville, FL','(904) 520-4650','','westmarine.com','Jacksonville','Duval'], + ['West Marine Jacksonville Beach','14180 Beach Blvd, Jacksonville, FL','(904) 821-5033','','westmarine.com','Jacksonville Beach','Duval'], + ['Specialty Marine & Industrial Supplies','1420 Mayport Rd, Atlantic Beach, FL','(904) 247-3303','','specialtymarinefl.com','Atlantic Beach','Duval'], + ['Main Street Marine','9211 N Main St, Jacksonville, FL','(904) 757-3100','','mainstmarine.com','Jacksonville','Duval'], + ['MarineMax Jacksonville','2079 Beach Blvd, Jacksonville Beach, FL','(904) 338-9970','','marinemax.com','Jacksonville Beach','Duval'], + ['Ocean Outboard Marine','1619 N 14th St, Fernandina Beach, FL','(904) 321-1422','','oceanoutboardmarine.com','Fernandina Beach','Nassau'], + ['Chelsea Boat Center','96114 David Hallman Pkwy, Yulee, FL','(904) 261-8884','','chelseamarinesupply.com','Yulee','Nassau'], +]; + +function buildSheet(data, sheetTitle) { + const rows = [[sheetTitle]]; + rows.push(HEADERS); + data.forEach((row, i) => { + rows.push([i + 1, ...row]); + }); + return xlsx.utils.aoa_to_sheet(rows); +} + +function main() { + console.log('Leyendo Excel...'); + const wb = xlsx.readFile(EXCEL_PATH); + + // Crear hojas nuevas para Florida Norte + const sheets = [ + { name: 'MARINAS_NORTE', data: MARINAS, title: 'Marinas — Florida Norte (Sesión 6)' }, + { name: 'ASTILLEROS_NORTE', data: ASTILLEROS, title: 'Astilleros — Florida Norte (Sesión 6)' }, + { name: 'ALMACENAMIENTO_NORTE', data: ALMACENAMIENTO, title: 'Almacenamiento — Florida Norte (Sesión 6)' }, + { name: 'SUMINISTROS_NORTE', data: SUMINISTROS, title: 'Suministros — Florida Norte (Sesión 6)' }, + ]; + + for (const s of sheets) { + if (wb.SheetNames.includes(s.name)) { + wb.SheetNames.splice(wb.SheetNames.indexOf(s.name), 1); + } + wb.Sheets[s.name] = buildSheet(s.data, s.title); + wb.SheetNames.push(s.name); + console.log(` ✅ ${s.name}: ${s.data.length} registros`); + } + + // Actualizar RESUMEN + const resWs = wb.Sheets['RESUMEN']; + const resData = xlsx.utils.sheet_to_json(resWs, { header: 1 }); + // Agregar fila de totales norte FL + resData.push(['Florida Norte', MARINAS.length, '-', '-', '-', '-', MARINAS.length, 'MARINAS_NORTE']); + resData.push(['FL Norte Astilleros', '-', '-', '-', '-', '-', ASTILLEROS.length, 'ASTILLEROS_NORTE']); + resData.push(['FL Norte Almacenamiento', '-', '-', '-', '-', '-', ALMACENAMIENTO.length, 'ALMACENAMIENTO_NORTE']); + resData.push(['FL Norte Suministros', '-', '-', '-', '-', '-', SUMINISTROS.length, 'SUMINISTROS_NORTE']); + wb.Sheets['RESUMEN'] = xlsx.utils.aoa_to_sheet(resData); + + xlsx.writeFile(wb, EXCEL_PATH); + console.log(`\n🎉 Guardado exitosamente en: ${EXCEL_PATH}`); + console.log(`\n📊 RESUMEN TOTAL:`); + console.log(` Marinas Norte FL: ${MARINAS.length}`); + console.log(` Astilleros Norte FL: ${ASTILLEROS.length}`); + console.log(` Almacenamiento Norte FL: ${ALMACENAMIENTO.length}`); + console.log(` Suministros Norte FL: ${SUMINISTROS.length}`); + console.log(` TOTAL NORTE FL: ${MARINAS.length + ASTILLEROS.length + ALMACENAMIENTO.length + SUMINISTROS.length}`); +} + +main(); diff --git a/Automatización Bot Prisa yachts/convert_to_pdf.js b/Automatización Bot Prisa yachts/convert_to_pdf.js new file mode 100644 index 0000000..b21d6aa --- /dev/null +++ b/Automatización Bot Prisa yachts/convert_to_pdf.js @@ -0,0 +1,21 @@ +const puppeteer = require('puppeteer'); +const path = require('path'); + +(async () => { + const browser = await puppeteer.launch({ headless: true }); + const page = await browser.newPage(); + + const htmlPath = path.resolve(__dirname, 'Prisa_Yachts_Brochure.html'); + await page.goto(`file:///${htmlPath.replace(/\\/g, '/')}`, { waitUntil: 'networkidle0' }); + + await page.pdf({ + path: path.resolve(__dirname, 'Prisa_Yachts_Brochure.pdf'), + format: 'Letter', + margin: { top: 0, right: 0, bottom: 0, left: 0 }, + printBackground: true, + preferCSSPageSize: true, + }); + + await browser.close(); + console.log('PDF generated: Prisa_Yachts_Brochure.pdf'); +})(); diff --git a/Automatización Bot Prisa yachts/create_whatsapp_workflow.js b/Automatización Bot Prisa yachts/create_whatsapp_workflow.js new file mode 100644 index 0000000..d40d206 --- /dev/null +++ b/Automatización Bot Prisa yachts/create_whatsapp_workflow.js @@ -0,0 +1,537 @@ +// create_whatsapp_workflow.js — Prisa Yachts WhatsApp Bot for n8n +const https = require('https'); + +const N8N_BASE = process.env.N8N_BASE_URL || 'https://n8n.crewinghunters.com'; +const API_KEY = process.env.N8N_API_KEY; +if (!API_KEY) { console.error('ERROR: N8N_API_KEY environment variable is required'); process.exit(1); } + +/* ─────────────────────────────────────────────────────────── helpers */ +function apiCall(method, path, body) { + return new Promise((resolve, reject) => { + const data = body ? JSON.stringify(body) : null; + const opts = { + hostname: 'n8n.crewinghunters.com', + path: '/api/v1' + path, + method, + headers: { + 'X-N8N-API-KEY': API_KEY, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + ...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}) + } + }; + const req = https.request(opts, res => { + let buf = ''; + res.on('data', c => buf += c); + res.on('end', () => { + try { resolve({ status: res.statusCode, data: JSON.parse(buf) }); } + catch { resolve({ status: res.statusCode, data: buf }); } + }); + }); + req.on('error', reject); + if (data) req.write(data); + req.end(); + }); +} + +/* ─────────────────────────────────────── state-machine code (runs inside n8n) */ +const STATE_MACHINE_CODE = ` +const WELCOME = \`👋 Welcome to Prisa Yachts LLC! +Bienvenido a Prisa Yachts LLC + +Safe Command ♦ Luxury Maintenance and Care + +Select your service / Selecciona tu servicio: + +1️⃣ Engines & Mechanical +2️⃣ Electrical & Electronics / NMEA +3️⃣ Teak Deck Recovery +4️⃣ Captaining & Crewing +5️⃣ Yacht Care & Detailing +6️⃣ Crew Placement & Staffing +7️⃣ Other / Otro + +Reply 1–7 / Responde 1–7\`; + +const CAT_MSGS = { + 1:\`🔧 Engines & Mechanical + +1. Vessel name & type? +2. Engine brand & approx. year? +3. Briefly describe the issue: + (noise, leak, won't start, overheating, + routine service, fuel system, etc.) +4. Current location / Marina? +5. When do you need service? + · Flexible · This week · ASAP + +───────────────── +Your contact info: +- Full name: +- Phone: +- Email: +───────────────── +Thank you! Our specialist will +contact you within 2 hours. 🛥️\`, + + 2:\`⚡ Electrical & Electronics / NMEA + +1. Vessel name & type? +2. Briefly describe the issue or installation: + (wiring, shore power, panel, NMEA 2000, + AIS, chartplotter, VHF, autopilot, etc.) +3. Is it a new installation or a repair? + · New install · Repair · Diagnosis +4. Current location / Marina? +5. When do you need service? + · Flexible · This week · ASAP + +───────────────── +Your contact info: +- Full name: +- Phone: +- Email: +───────────────── +Thank you! Our specialist will +contact you within 2 hours. 🛥️\`, + + 3:\`🪵 Teak Deck Recovery + +1. Vessel name & LOA (feet)? +2. Teak condition: + · Cleaning needed · Caulking damaged + · Planks cracked · Full replacement +3. Which deck areas? + (cockpit, flybridge, side decks, all) +4. Briefly describe the current condition: +5. Current location / Marina? +6. When do you need service? + · Flexible · This week · ASAP + +───────────────── +Your contact info: +- Full name: +- Phone: +- Email: +───────────────── +Thank you! Our specialist will +contact you within 2 hours. 🛥️\`, + + 4:\`⚓ Captaining & Crewing + +1. Vessel name, type & LOA (feet)? +2. Service needed: + · Day trip · Charter · Vessel delivery + · Offshore passage · Event crew +3. Departure & destination? +4. Date & duration? +5. Number of crew needed? +6. Any special requirements? + +───────────────── +Your contact info: +- Full name: +- Phone: +- Email: +───────────────── +Thank you! Our captain will +contact you within 2 hours. 🛥️\`, + + 5:\`🛥️ Yacht Care & Detailing + +1. Vessel name, type & LOA (feet)? +2. Service needed: + · Full wash & wax · Hull polish + · Interior cleaning · Brightwork + · Antifouling prep · Full detail +3. Briefly describe current condition: +4. Current location / Marina? +5. When do you need service? + · Flexible · This week · ASAP + +───────────────── +Your contact info: +- Full name: +- Phone: +- Email: +───────────────── +Thank you! Our team will +contact you within 2 hours. 🛥️\`, + + 6:\`👥 Crew Placement & Staffing + +1. Vessel name, type & LOA (feet)? +2. Position(s) needed: + · Captain · Chief Mate / Deck Officer + · Deckhand · Chief Engineer + · Stewardess / Interior crew + · Chef · Other +3. Contract type: + · Permanent · Seasonal · One-way delivery + · Day work · Relief / Temporary +4. Start date & duration? +5. Vessel flag & trading area? +6. Any certifications required? + (STCW, license type, experience level) + +───────────────── +Your contact info: +- Full name: +- Phone: +- Email: +- Company (if applicable): +───────────────── +Thank you! Our crewing team will +contact you within 2 hours. 🛥️\`, + + 7:\`📋 Other / Otro + +1. Vessel name & type? +2. Briefly describe what you need: +3. Current location / Marina? +4. When do you need service? + · Flexible · This week · ASAP + +───────────────── +Your contact info: +- Full name: +- Phone: +- Email: +───────────────── +Thank you! Our team will +contact you within 2 hours. 🛥️\` +}; + +const CONFIRMATION = \`✅ Request received! / ¡Solicitud recibida! + +We have all your information and a +Prisa Yachts specialist will contact +you within 2 hours. + +📞 (786) 396-3315 +📧 info@prisayachts.com +🌐 prisayachts.com +📸 @prisayachts + +Safe Command ♦ Luxury Maintenance and Care\`; + +const CAT_NAMES = { + 1:'Engines & Mechanical', 2:'Electrical & Electronics / NMEA', + 3:'Teak Deck Recovery', 4:'Captaining & Crewing', + 5:'Yacht Care & Detailing', 6:'Crew Placement & Staffing', 7:'Other / Otro' +}; + +const ROUTING = {1:'alvaro',2:'alvaro',3:'federico',4:'federico',5:'federico',6:'federico',7:'both'}; +const ALVARO = '19546554084'; +const FEDERICO = '17542093375'; +const RESET_WORDS = ['menu','start','hola','hello','hi']; +const TIMEOUT_MS = 24 * 60 * 60 * 1000; + +// ── parse webhook payload ────────────────────────────────────────── +const raw = $input.first().json; + +// Meta GET verification +const q = raw.query || {}; +if (q['hub.challenge']) { + return [{ json: { action: 'webhook_verify', challenge: String(q['hub.challenge']) } }]; +} + +const body = raw.body || raw; +const msg = body?.entry?.[0]?.changes?.[0]?.value?.messages?.[0]; +if (!msg || msg.type !== 'text') { + return [{ json: { action: 'ignore' } }]; +} + +const from = msg.from; +const msgText = msg.text.body.trim(); +const msgLow = msgText.toLowerCase(); + +// ── state machine ───────────────────────────────────────────────── +const sd = $getWorkflowStaticData('global'); +if (!sd.states) sd.states = {}; + +const now = Date.now(); +let state = sd.states[from] || { step: 'new', lastActivity: 0, data: {} }; +if (now - state.lastActivity > TIMEOUT_MS) state = { step: 'new', lastActivity: now, data: {} }; +state.lastActivity = now; + +const reply = (messageText, extra={}) => { + sd.states[from] = state; + return [{ json: { action: 'send_to_client', to: from, messageText, notifySpecialist: false, ...extra } }]; +}; + +if (RESET_WORDS.includes(msgLow) || state.step === 'new') { + state.step = 'waiting_category'; state.data = {}; + return reply(WELCOME); +} + +if (state.step === 'waiting_category') { + const cat = parseInt(msgText, 10); + if (cat >= 1 && cat <= 7) { + state.step = 'collecting_info'; state.data.category = cat; + return reply(CAT_MSGS[cat]); + } + return reply(WELCOME); +} + +if (state.step === 'collecting_info') { + const cat = state.data.category; + const ts = new Date().toLocaleString('en-US', { timeZone: 'America/New_York' }); + const specMsg = \`🔔 NEW SERVICE REQUEST — Prisa Yachts + +📋 Category: \${CAT_NAMES[cat]} +👤 Client WhatsApp: +\${from} + +📝 Info received: +\${msgText} + +🕐 Received: \${ts} ET\`; + + state.step = 'complete'; + return reply(CONFIRMATION, { + notifySpecialist: true, + routing: ROUTING[cat], + specialistMsg: specMsg, + alvaroPhone: ALVARO, + federicoPhone: FEDERICO + }); +} + +// complete → restart +state.step = 'waiting_category'; state.data = {}; +return reply(WELCOME); +`; + +/* ────────────────────────────────────────── build workflow JSON */ +function buildWorkflow(credId) { + const credential = { httpHeaderAuth: { id: String(credId), name: 'WhatsApp_Prisa' } }; + const waUrl = 'https://graph.facebook.com/v18.0/PLACEHOLDER_PHONE_NUMBER_ID/messages'; + + const ifStrNode = (id, name, pos, left, right) => ({ + id, name, + type: 'n8n-nodes-base.if', typeVersion: 2, + position: pos, + parameters: { + conditions: { combinator:'and', conditions:[{ + leftValue: left, rightValue: right, + operator: { type:'string', operation:'equals' } + }]}, + options: {} + } + }); + + const httpNode = (id, name, pos, jsonBody) => ({ + id, name, + type: 'n8n-nodes-base.httpRequest', typeVersion: 4.2, + position: pos, + credentials: credential, + parameters: { + method: 'POST', url: waUrl, + authentication: 'genericCredentialType', + genericAuthType: 'httpHeaderAuth', + sendBody: true, specifyBody: 'json', + jsonBody, options: {} + } + }); + + const clientBody = `={{ JSON.stringify({messaging_product:"whatsapp",to:$json.to,type:"text",text:{body:$json.messageText}}) }}`; + const alvaroBody = `={{ JSON.stringify({messaging_product:"whatsapp",to:$json.alvaroPhone,type:"text",text:{body:$json.specialistMsg}}) }}`; + const fedBody = `={{ JSON.stringify({messaging_product:"whatsapp",to:$json.federicoPhone,type:"text",text:{body:$json.specialistMsg}}) }}`; + + return { + name: 'Prisa Yachts — WhatsApp Bot', + nodes: [ + // 1 Webhook + { + id: 'n-wh', name: 'WhatsApp Webhook', + type: 'n8n-nodes-base.webhook', typeVersion: 2, + position: [200, 300], + parameters: { httpMethod:'POST', path:'prisa-whatsapp', responseMode:'onReceived', options:{} } + }, + // 2 State machine + { + id: 'n-sm', name: 'State Machine', + type: 'n8n-nodes-base.code', typeVersion: 2, + position: [450, 300], + parameters: { mode:'runOnceForAllItems', jsCode: STATE_MACHINE_CODE } + }, + // 3 IF: is webhook_verify? + ifStrNode('n-ifv', 'Is Verify?', [700, 300], '={{ $json.action }}', 'webhook_verify'), + // 4 Respond with challenge + { + id: 'n-respond', name: 'Respond — Verify', + type: 'n8n-nodes-base.respondToWebhook', typeVersion: 1.1, + position: [950, 140], + parameters: { + respondWith: 'text', + responseBody: '={{ $json.challenge }}', + options: { responseCode: 200 } + } + }, + // 5 IF: is send_to_client? + ifStrNode('n-ifs', 'Is Send?', [950, 380], '={{ $json.action }}', 'send_to_client'), + // 6 Send to client + httpNode('n-hsc', 'Send WhatsApp — Client', [1200, 280], clientBody), + // 7 Ignore (no-op) + { id:'n-nop', name:'Ignore', type:'n8n-nodes-base.noOp', typeVersion:1, position:[1200,520], parameters:{} }, + // 8 IF notifySpecialist + { + id: 'n-ifn', name: 'Notify Specialist?', + type: 'n8n-nodes-base.if', typeVersion: 2, + position: [1450, 280], + parameters: { + conditions: { combinator:'and', conditions:[{ + leftValue:'={{ $json.notifySpecialist }}', rightValue: true, + operator:{ type:'boolean', operation:'equals' } + }]}, + options: {} + } + }, + // 9 IF routing === 'both' → true: both, false: check alvaro/fed + ifStrNode('n-ifb', 'Is Both?', [1700, 200], '={{ $json.routing }}', 'both'), + // 10 IF routing === 'alvaro' → true: alvaro only, false: federico only + ifStrNode('n-ifa', 'Is Alvaro?', [1700, 420], '={{ $json.routing }}', 'alvaro'), + // 11,12 HTTP nodes + httpNode('n-hal', 'Send WhatsApp — Alvaro', [1950, 180], alvaroBody), + httpNode('n-hfe', 'Send WhatsApp — Federico', [1950, 420], fedBody), + ], + connections: { + 'WhatsApp Webhook': { main: [[{ node:'State Machine', type:'main', index:0 }]] }, + 'State Machine': { main: [[{ node:'Is Verify?', type:'main', index:0 }]] }, + 'Is Verify?': { main: [ + [{ node:'Respond — Verify', type:'main', index:0 }], // true + [{ node:'Is Send?', type:'main', index:0 }] // false + ]}, + 'Is Send?': { main: [ + [{ node:'Send WhatsApp — Client', type:'main', index:0 }], // true + [{ node:'Ignore', type:'main', index:0 }] // false + ]}, + 'Send WhatsApp — Client': { main: [[{ node:'Notify Specialist?', type:'main', index:0 }]] }, + 'Notify Specialist?': { main: [ + [{ node:'Is Both?', type:'main', index:0 }], // true → route + [] // false → end + ]}, + 'Is Both?': { main: [ + // true → send to BOTH simultaneously + [{ node:'Send WhatsApp — Alvaro', type:'main', index:0 }, { node:'Send WhatsApp — Federico', type:'main', index:0 }], + [{ node:'Is Alvaro?', type:'main', index:0 }] // false → check which one + ]}, + 'Is Alvaro?': { main: [ + [{ node:'Send WhatsApp — Alvaro', type:'main', index:0 }], // true + [{ node:'Send WhatsApp — Federico', type:'main', index:0 }] // false (must be federico) + ]} + }, + settings: { executionOrder: 'v1' } + }; +} + +/* ──────────────────────────────────────────────────────────── main */ +async function cleanup() { + const wfs = await apiCall('GET', '/workflows?limit=50'); + for (const wf of (wfs.data?.data || [])) { + if (wf.name && wf.name.includes('Prisa Yachts')) { + await apiCall('DELETE', `/workflows/${wf.id}`); + console.log(` 🗑 Deleted workflow: ${wf.id}`); + } + } + const creds = await apiCall('GET', '/credentials?limit=50'); + for (const c of (creds.data?.data || [])) { + if (c.name === 'WhatsApp_Prisa') { + await apiCall('DELETE', `/credentials/${c.id}`); + console.log(` 🗑 Deleted credential: ${c.id}`); + } + } +} + +async function main() { + console.log('\n🚀 Prisa Yachts — WhatsApp Bot Deployment\n'); + + console.log('0/4 Cleaning up previous attempts...'); + await cleanup(); + console.log(' ✅ Clean'); + + // 1. Credential + console.log('1/4 Creating WhatsApp credential...'); + const credRes = await apiCall('POST', '/credentials', { + name: 'WhatsApp_Prisa', + type: 'httpHeaderAuth', + data: { name: 'Authorization', value: 'Bearer PLACEHOLDER_PENDING_META_APPROVAL' } + }); + if (![200,201].includes(credRes.status)) { + console.error(' ❌ Credential error:', JSON.stringify(credRes.data, null, 2)); + process.exit(1); + } + const credId = credRes.data.id; + console.log(` ✅ Credential created id=${credId}`); + + // 2. Workflow + console.log('2/4 Creating workflow...'); + const wfRes = await apiCall('POST', '/workflows', buildWorkflow(credId)); + if (![200,201].includes(wfRes.status)) { + console.error(' ❌ Workflow error:', JSON.stringify(wfRes.data, null, 2)); + process.exit(1); + } + const wfId = wfRes.data.id; + console.log(` ✅ Workflow created id=${wfId}`); + + // 3. Activate + console.log('3/4 Activating workflow...'); + const actRes = await apiCall('POST', `/workflows/${wfId}/activate`); + if (actRes.status !== 200) { + console.warn(' ⚠️ Activation returned', actRes.status, JSON.stringify(actRes.data)); + } else { + console.log(' ✅ Workflow ACTIVE'); + } + + // 4. Confirm + console.log('4/4 Verifying...'); + const chk = await apiCall('GET', `/workflows/${wfId}`); + console.log(` ✅ active = ${chk.data?.active}`); + + console.log(` +══════════════════════════════════════════════════════ + ✅ DEPLOYMENT COMPLETE +══════════════════════════════════════════════════════ + + Webhook URL → https://n8n.crewinghunters.com/webhook/prisa-whatsapp + Workflow ID → ${wfId} + Credential → WhatsApp_Prisa (id=${credId}) + +══════════════════════════════════════════════════════ + NEXT STEPS — after Meta approves your WhatsApp account +══════════════════════════════════════════════════════ + + 1. n8n → Settings → Credentials → WhatsApp_Prisa + Change value from PLACEHOLDER_PENDING_META_APPROVAL + to Bearer + + 2. Open workflow "Prisa Yachts — WhatsApp Bot" + In these 3 nodes, change the URL: + • Send WhatsApp — Client + • Send WhatsApp — Alvaro + • Send WhatsApp — Federico + Replace PLACEHOLDER_PHONE_NUMBER_ID + With + + 3. Meta Developer Portal → WhatsApp → Configuration + Webhook URL : https://n8n.crewinghunters.com/webhook/prisa-whatsapp + Verify Token : any string (e.g. "prisayachts2025") + Subscribe to : messages + + 4. Send "hola" to +17863963315 from any WhatsApp to test. + +══════════════════════════════════════════════════════ + ROUTING +══════════════════════════════════════════════════════ + 1 Engines & Mechanical → Alvaro +19546554084 + 2 Electrical & Electronics → Alvaro +19546554084 + 3 Teak Deck Recovery → Federico +17542093375 + 4 Captaining & Crewing → Federico +17542093375 + 5 Yacht Care & Detailing → Federico +17542093375 + 6 Crew Placement & Staffing → Federico +17542093375 + 7 Other / Otro → BOTH +══════════════════════════════════════════════════════ +`); +} + +main().catch(e => { console.error('Fatal:', e); process.exit(1); }); diff --git a/Automatización Bot Prisa yachts/package-lock.json b/Automatización Bot Prisa yachts/package-lock.json new file mode 100644 index 0000000..4d18f13 --- /dev/null +++ b/Automatización Bot Prisa yachts/package-lock.json @@ -0,0 +1,1076 @@ +{ + "name": "Automatización Bot Prisa yachts", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "axios": "^1.18.1", + "cheerio": "^1.2.0", + "puppeteer": "^25.1.0", + "xlsx": "^0.18.5" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.4.tgz", + "integrity": "sha512-HGM8iAmGTf+Y7t0373szVbTmt3d7vPkYL/1bpOkOFO0YUYLgSeuYBCzESklogNPvOBnZ/MRD5f07OkpqH1trtA==", + "license": "Apache-2.0", + "dependencies": { + "modern-tar": "^0.7.6", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/main-cli.js" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + } + } + }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chromium-bidi": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz", + "integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1624250", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1624250.tgz", + "integrity": "sha512-YFAat/lOiIk0ARmBweG+ygrEcbZrq5B9urRyUoeQKp53MlidHXE2TmTbxKcaXoQj7u/aX+jebDO4BW55rs0WwA==", + "license": "BSD-3-Clause" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/modern-tar": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz", + "integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/puppeteer": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.1.0.tgz", + "integrity": "sha512-7L6/0JM7XStK99lIL4xQySyNEXNfII6pk0BxkI5kKBTOhR7AsoQiv067YTsE/rIXxQiq9ajlO4WcqBjS/FWK1A==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.0.4", + "chromium-bidi": "16.0.1", + "devtools-protocol": "0.0.1624250", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.1.0", + "typed-query-selector": "^2.12.2" + }, + "bin": { + "puppeteer": "lib/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/puppeteer-core": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.1.0.tgz", + "integrity": "sha512-jKzy5y4WG6uNuFbTWgW1D7mqoT9o0nllc/6a1DGF775T1mPmgw3scdFEtEq67yVFikavQmbYq6NLfbTfxHSlqQ==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.0.4", + "chromium-bidi": "16.0.1", + "devtools-protocol": "0.0.1624250", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.2", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz", + "integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==", + "license": "Apache-2.0" + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/Automatización Bot Prisa yachts/package.json b/Automatización Bot Prisa yachts/package.json new file mode 100644 index 0000000..f8e19b9 --- /dev/null +++ b/Automatización Bot Prisa yachts/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "axios": "^1.18.1", + "cheerio": "^1.2.0", + "puppeteer": "^25.1.0", + "xlsx": "^0.18.5" + } +} diff --git a/Automatización Bot Prisa yachts/scrape_marine_emails.js b/Automatización Bot Prisa yachts/scrape_marine_emails.js new file mode 100644 index 0000000..4a831cc --- /dev/null +++ b/Automatización Bot Prisa yachts/scrape_marine_emails.js @@ -0,0 +1,315 @@ +// scrape_marine_emails.js +// Reads marine business directory Excel, searches for emails/websites, +// writes results back to the same file with checkpoints every 25 rows. + +const xlsx = require('xlsx'); +const axios = require('axios'); +const cheerio = require('cheerio'); +const path = require('path'); +const fs = require('fs'); + +const EXCEL_PATH = path.resolve(__dirname, 'Directorio_Marino_FL_Acumulativo_4.xlsx'); +const SHEETS = ['MARINAS', 'ASTILLEROS', 'ALMACENAMIENTO', 'SUMINISTROS']; +const CHECKPOINT = 25; +const DELAY_MS = 2000; +const TIMEOUT_MS = 10000; +const MAX_PAGES = 3; + +const COL = { NUM:0, NAME:1, ADDR:2, PHONE:3, EMAIL:4, CITY:5, COUNTY:6, WEBSITE:7 }; + +const EMAIL_RE = /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g; + +const summary = { processed:0, emails:0, websites:0, noEmail:[] }; + +const http = axios.create({ + timeout: TIMEOUT_MS, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5' + }, + maxRedirects: 5, + validateStatus: s => s < 500 +}); + +function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } + +// Guard 1: sanitize names before using in search queries +function sanitize(str) { + if (!str) return ''; + return String(str).replace(/[<>"'`;&|$\\{}]/g, '').trim().substring(0, 100); +} + +function extractEmails(text) { + const raw = text.match(EMAIL_RE) || []; + return [...new Set(raw.filter(e => { + const low = e.toLowerCase(); + return !low.match(/\.(png|jpg|jpeg|gif|svg|webp|pdf|zip|js|css)$/) && + !low.includes('example.com') && + !low.includes('sentry.io') && + low.includes('.'); + }))]; +} + +function scoreEmail(email) { + const low = email.toLowerCase(); + if (/dockmaster@/.test(low)) return 5; + if (/marina@/.test(low)) return 4; + if (/service@/.test(low)) return 3; + if (/contact@/.test(low)) return 2; + if (/info@/.test(low)) return 1; + return 0; +} + +function bestEmail(emails) { + if (!emails.length) return null; + return emails.sort((a, b) => scoreEmail(b) - scoreEmail(a))[0]; +} + +async function fetchPage(url) { + try { + if (!url.startsWith('http')) url = 'https://' + url; + const res = await http.get(url); + if (res.status === 200 && res.data) return { html: String(res.data), url }; + } catch { /* timeout or network error — skip */ } + return null; +} + +async function searchDDG(query) { + const q = encodeURIComponent(sanitize(query)); + try { + const res = await http.get(`https://html.duckduckgo.com/html/?q=${q}&kl=us-en`); + if (!res.data) return []; + const $ = cheerio.load(res.data); + const results = []; + + // Primary: result URL spans + $('a.result__url').each((_, el) => { + let href = $(el).attr('href') || $(el).text().trim(); + if (href && !href.includes('duckduckgo.com')) { + if (!href.startsWith('http')) href = 'https://' + href; + results.push(href); + } + }); + + // Fallback: uddg= redirect links + if (!results.length) { + $('a[href*="uddg="]').each((_, el) => { + const href = $(el).attr('href') || ''; + try { + const u = new URL(href, 'https://duckduckgo.com'); + const dest = u.searchParams.get('uddg'); + if (dest) results.push(decodeURIComponent(dest)); + } catch {} + }); + } + + return [...new Set(results)].slice(0, 6); + } catch { return []; } +} + +function rootDomain(url) { + try { return new URL(url).origin; } catch { return null; } +} + +const SKIP_DOMAINS = /yelp|yellowpages|mapquest|tripadvisor|facebook|google|instagram|twitter|linkedin|bbb\.org|manta\.com|chamberofcommerce|indeed|glassdoor|angieslist|homeadvisor|findagrave/i; + +async function scrapeBusiness(name, city) { + const query = `"${name}" ${city} Florida marina contact email`; + let website = null; + let email = null; + let pagesFetched = 0; + const visitedDomains = new Set(); + + const results = await searchDDG(query); + await sleep(300); + + for (const url of results) { + if (SKIP_DOMAINS.test(url)) continue; + const domain = rootDomain(url); + if (!domain || visitedDomains.has(domain)) continue; + visitedDomains.add(domain); + + if (!website) website = url; + + // Fetch homepage + if (pagesFetched >= MAX_PAGES) break; + const home = await fetchPage(url); + pagesFetched++; + if (!home) continue; + + const e1 = extractEmails(home.html); + if (e1.length) { email = bestEmail(e1); break; } + + // Fetch /contact page + if (pagesFetched < MAX_PAGES) { + const contactUrl = domain + '/contact'; + const contact = await fetchPage(contactUrl) || + await fetchPage(domain + '/contact-us'); + pagesFetched++; + if (contact) { + const e2 = extractEmails(contact.html); + if (e2.length) { email = bestEmail(e2); break; } + } + } + + // Fetch /about page + if (pagesFetched < MAX_PAGES && !email) { + const about = await fetchPage(domain + '/about') || + await fetchPage(domain + '/about-us'); + pagesFetched++; + if (about) { + const e3 = extractEmails(about.html); + if (e3.length) { email = bestEmail(e3); break; } + } + } + + if (email) break; + } + + return { website, email }; +} + +function saveWorkbook(wb) { + const tmp = EXCEL_PATH + '.tmp'; + try { + xlsx.writeFile(wb, tmp); + if (fs.existsSync(tmp)) { + fs.copyFileSync(tmp, EXCEL_PATH); + fs.unlinkSync(tmp); + } + } catch (err) { + // If original is locked by Excel, save to sidecar so no progress is lost + const sidecar = EXCEL_PATH.replace('.xlsx', '_output.xlsx'); + console.log(`\n ⚠️ File locked — saving to: ${path.basename(sidecar)}`); + xlsx.writeFile(wb, sidecar); + } +} + +async function processSheet(wb, sheetName) { + const ws = wb.Sheets[sheetName]; + const data = xlsx.utils.sheet_to_json(ws, { header:1, defval:'' }); + + // Find the header row (contains "NOMBRE") — could be row 0 or 1 + let headerIdx = 0; + for (let r = 0; r < Math.min(3, data.length); r++) { + if (data[r] && data[r].includes('NOMBRE')) { headerIdx = r; break; } + } + if (data[headerIdx] && !data[headerIdx][COL.WEBSITE]) data[headerIdx][COL.WEBSITE] = 'WEBSITE'; + + const dataStart = headerIdx + 1; + console.log(`\n${'═'.repeat(60)}`); + console.log(` Sheet: ${sheetName} (${data.length - dataStart} data rows, header at row ${headerIdx})`); + console.log(`${'═'.repeat(60)}`); + + let rowsSinceCheckpoint = 0; + + for (let i = dataStart; i < data.length; i++) { + const row = data[i]; + const name = sanitize(row[COL.NAME]); + const city = sanitize(row[COL.CITY]); + + if (!name) continue; + + // Skip rows already complete + if (row[COL.EMAIL] && row[COL.WEBSITE]) { + console.log(` row ${String(i).padStart(3)} SKIP (filled): ${name.substring(0,35)}`); + summary.processed++; + continue; + } + + process.stdout.write(` row ${String(i).padStart(3)} ${name.substring(0,38).padEnd(38)} → `); + + let email = row[COL.EMAIL] || null; + let website = row[COL.WEBSITE] || null; + + try { + if (!email || !website) { + const result = await scrapeBusiness(name, city); + if (!email && result.email) email = result.email; + if (!website && result.website) website = result.website; + } + } catch (err) { + process.stdout.write(`ERR(${err.message.substring(0,30)})\n`); + } + + if (email) row[COL.EMAIL] = email; + if (website) row[COL.WEBSITE] = website; + + summary.processed++; + if (email) summary.emails++; + if (website) summary.websites++; + if (!email) summary.noEmail.push(`[${sheetName}] ${name} — ${city}`); + + process.stdout.write(`${email || '(no email)'.padEnd(35)} ${website ? website.substring(0,40) : '(no site)'}\n`); + + const newWs = xlsx.utils.aoa_to_sheet(data); + wb.Sheets[sheetName] = newWs; + + rowsSinceCheckpoint++; + if (rowsSinceCheckpoint >= CHECKPOINT) { + saveWorkbook(wb); + console.log(`\n ★ CHECKPOINT: saved after ${summary.processed} total rows processed ★\n`); + rowsSinceCheckpoint = 0; + } + + await sleep(DELAY_MS); + } + + const newWs = xlsx.utils.aoa_to_sheet(data); + wb.Sheets[sheetName] = newWs; + saveWorkbook(wb); + console.log(`\n ✅ ${sheetName} complete — file saved.`); +} + +async function main() { + console.log(` +✅ GUARD 1 — Input: business names sanitized before search queries +✅ GUARD 2 — Network: ${TIMEOUT_MS/1000}s timeout, max ${MAX_PAGES} pages/domain, ${DELAY_MS/1000}s inter-domain delay +✅ GUARD 3 — Data: reads/writes only ${path.basename(EXCEL_PATH)}, no credentials stored + +🚀 Starting marine directory email scraper... + File: ${EXCEL_PATH} + Sheets: ${SHEETS.join(', ')} +`); + + if (!fs.existsSync(EXCEL_PATH)) { + console.error('❌ Excel file not found:', EXCEL_PATH); + process.exit(1); + } + + const wb = xlsx.readFile(EXCEL_PATH); + const start = Date.now(); + + for (const sheet of SHEETS) { + if (!wb.SheetNames.includes(sheet)) { + console.log(`Sheet "${sheet}" not found — skipping.`); + continue; + } + await processSheet(wb, sheet); + } + + saveWorkbook(wb); + const elapsed = Math.round((Date.now() - start) / 1000); + + const noEmailList = summary.noEmail.length + ? summary.noEmail.map(n => ' • ' + n).join('\n') + : ' (all businesses have email)'; + + console.log(` +${'═'.repeat(60)} + FINAL SUMMARY +${'═'.repeat(60)} + Rows processed : ${summary.processed} + Emails found : ${summary.emails} (${Math.round(summary.emails/summary.processed*100)||0}%) + Websites found : ${summary.websites} (${Math.round(summary.websites/summary.processed*100)||0}%) + Time elapsed : ${Math.floor(elapsed/60)}m ${elapsed%60}s + Saved to : ${EXCEL_PATH} + + Businesses with NO email (${summary.noEmail.length}) — for manual follow-up: +${noEmailList} +${'═'.repeat(60)} +`); +} + +main().catch(err => { console.error('Fatal:', err); process.exit(1); }); diff --git a/Calendario Voz IA/BLUEPRINT.md b/Calendario Voz IA/BLUEPRINT.md new file mode 100644 index 0000000..9aa0cdf --- /dev/null +++ b/Calendario Voz IA/BLUEPRINT.md @@ -0,0 +1,518 @@ +# Voz → Calendario Nextcloud — Blueprint Completo +**Versión:** 1.0 +**Fecha:** 2026-07-03 +**Autor:** Álvaro +**n8n:** https://n8n.crewinghunters.com + +--- + +## Visión General + +Grabas un audio en el teléfono → se lo mandas al bot de Telegram → la IA local transcribe y entiende el evento → aparece en tu calendario de Nextcloud → te recuerda 30 minutos antes por Telegram. + +``` +📱 Audio (teléfono) + │ + ▼ Telegram +┌─────────────────────────────────────────────────┐ +│ n8n (Raspberry Pi 4 — Colombia) │ +│ │ +│ 1. Recibe audio de Telegram │ +│ 2. Descarga el archivo .ogg/.mp3 │ +│ 3. Llama a Whisper API (tu PC via Tailscale) │ +│ 4. Llama a Ollama (tu PC via Tailscale) │ +│ 5. Crea evento en Nextcloud Calendar (CalDAV) │ +│ 6. Confirma por Telegram │ +│ 7. Programa recordatorio 30min antes │ +└─────────────────────────────────────────────────┘ + │ + ▼ Tailscale VPN (túnel privado gratis) +┌─────────────────────────────────────────────────┐ +│ Tu PC (Windows) │ +│ ├── Ollama → AsistentePersonal / qwen2.5:14b │ +│ └── Whisper API → faster-whisper (Python) │ +└─────────────────────────────────────────────────┘ +``` + +--- + +## Stack Tecnológico + +| Componente | Herramienta | Dónde corre | +|-----------|------------|------------| +| Automatización | n8n | RPi4 Colombia | +| Bot mensajería | Telegram Bot | Cloud (Telegram) | +| Transcripción voz | faster-whisper | Tu PC (Windows) | +| LLM extracción evento | Ollama / qwen2.5:14b | Tu PC (Windows) | +| Calendario | Nextcloud CalDAV | RPi4 Colombia | +| Túnel privado | Tailscale | PC + RPi4 | +| API transcripción | FastAPI (Python) | Tu PC (Windows) | + +--- + +## PASO 1 — Tailscale (conectar PC con RPi4) + +Tailscale crea una red privada entre tus dispositivos. Una vez instalado, tu PC tendrá una IP tipo `100.x.x.x` accesible desde el RPi4 sin abrir puertos ni router. + +### En tu PC (Windows) +1. Descarga e instala Tailscale: https://tailscale.com/download/windows +2. Inicia sesión con Google o email +3. Anota tu IP Tailscale: aparece en el icono de la bandeja del sistema + - Ejemplo: `100.64.1.10` + +### En el RPi4 (via SSH desde Colombia) +```bash +# Conectar al RPi4 +ssh usuario@n8n.crewinghunters.com + +# Instalar Tailscale en RPi4 +curl -fsSL https://tailscale.com/install.sh | sh + +# Iniciar y autenticar (con la MISMA cuenta que en el PC) +sudo tailscale up + +# Verificar que ve tu PC +tailscale ping 100.64.1.10 # ← IP de tu PC Tailscale +``` + +✅ Si el ping responde, están conectados. El RPi4 puede hablar con tu PC. + +--- + +## PASO 2 — Whisper API en tu PC + +Instalamos faster-whisper y levantamos una API HTTP que n8n puede llamar. + +### Instalar faster-whisper +```bash +# En tu PC (PowerShell) +pip install faster-whisper fastapi uvicorn python-multipart +``` + +### Crear el servidor API + +Guarda este archivo en: `C:\whisper-api\server.py` + +```python +from fastapi import FastAPI, UploadFile, File +from faster_whisper import WhisperModel +import tempfile, os + +app = FastAPI() + +# Carga el modelo una vez al iniciar (medium es bueno para español) +# Opciones: "tiny", "base", "small", "medium", "large-v3" +model = WhisperModel("medium", device="cpu", compute_type="int8") + +@app.post("/transcribe") +async def transcribe(file: UploadFile = File(...)): + # Guardar audio temporalmente + suffix = os.path.splitext(file.filename)[1] or ".ogg" + with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp: + tmp.write(await file.read()) + tmp_path = tmp.name + + # Transcribir + segments, info = model.transcribe(tmp_path, language="es") + text = " ".join([seg.text for seg in segments]).strip() + + os.unlink(tmp_path) + return {"text": text, "language": info.language} + +@app.get("/health") +def health(): + return {"status": "ok"} +``` + +### Script para iniciar la API (guarda como `C:\whisper-api\start.bat`) +```bat +@echo off +cd C:\whisper-api +uvicorn server:app --host 0.0.0.0 --port 8765 +``` + +### Iniciar automáticamente con Windows +1. Presiona `Win + R` → escribe `shell:startup` +2. Crea un acceso directo a `start.bat` en esa carpeta +3. La API estará disponible en: `http://100.64.1.10:8765` (via Tailscale) + +### Verificar que funciona (desde RPi4) +```bash +# Desde el RPi4 via SSH +curl http://100.64.1.10:8765/health +# Debe responder: {"status":"ok"} +``` + +--- + +## PASO 3 — Exponer Ollama via Tailscale + +Por defecto Ollama solo escucha en localhost. Hay que permitir que escuche en todas las interfaces. + +### En tu PC (PowerShell como Administrador) +```powershell +# Agregar variable de entorno para Ollama +[System.Environment]::SetEnvironmentVariable("OLLAMA_HOST", "0.0.0.0:11434", "Machine") + +# Reiniciar Ollama (busca en la bandeja del sistema → clic derecho → Quit, luego reabre) +``` + +### Verificar desde RPi4 +```bash +curl http://100.64.1.10:11434/api/tags +# Debe listar tus modelos +``` + +--- + +## PASO 4 — Bot de Telegram + +### Crear el bot +1. Abre Telegram → busca `@BotFather` +2. Escribe `/newbot` +3. Nombre del bot: `Mi Calendario Personal` (o el que quieras) +4. Username: `mi_calendario_alvaro_bot` (debe terminar en `_bot`) +5. BotFather te da un **token** — guárdalo: `7123456789:AAF...` + +### Obtener tu Chat ID +1. Escribe cualquier mensaje a tu nuevo bot +2. Abre en el navegador (reemplaza TOKEN): + ``` + https://api.telegram.org/botTOKEN/getUpdates + ``` +3. Busca `"id"` dentro de `"chat"` — ese número es tu **Chat ID** + - Ejemplo: `123456789` + +--- + +## PASO 5 — Nextcloud Calendar (CalDAV) + +n8n se conecta al calendario de Nextcloud via CalDAV. + +### Obtener la URL CalDAV +En Nextcloud → Calendario → ⚙️ (engranaje junto al calendario) → "Copiar enlace privado" + +La URL tiene esta forma: +``` +https://tu-nextcloud.com/remote.php/dav/calendars/USUARIO/personal/ +``` + +### Credenciales +- Usuario: tu usuario de Nextcloud +- Contraseña: genera una **contraseña de aplicación** en Nextcloud: + Configuración → Seguridad → Dispositivos y sesiones → "Crear nueva contraseña de aplicación" + - Nombre: `n8n-calendario` + +--- + +## PASO 6 — Organizar Carpetas en n8n + +Antes de crear el workflow, organiza las carpetas: + +1. Abre https://n8n.crewinghunters.com +2. En el panel izquierdo → "Workflows" → "Add folder" +3. Crea estas carpetas: + +``` +📁 Prisa Yachts ← mueve el bot de WhatsApp aquí +📁 Personal - Productividad +📁 AIS Navigator +📁 Pruebas / Sandbox +``` + +4. El nuevo workflow irá en `📁 Personal - Productividad` + +--- + +## PASO 7 — Workflow n8n Completo + +Nombre del workflow: `🎙️ Voz → Calendario Nextcloud` +Carpeta: `Personal - Productividad` + +### Nodos del workflow (en orden): + +``` +[Telegram Trigger] + │ + ▼ +[IF: ¿es audio?] + │ sí + ▼ +[HTTP: Descargar audio de Telegram] + │ + ▼ +[HTTP: Whisper API → transcripción] + │ + ▼ +[HTTP: Ollama → extraer evento] + │ + ▼ +[Code: parsear JSON respuesta Ollama] + │ + ▼ +[HTTP: Crear evento CalDAV en Nextcloud] + │ + ▼ +[Telegram: Confirmar al usuario] + │ + ▼ +[Wait: esperar hasta 30min antes del evento] + │ + ▼ +[Telegram: Enviar recordatorio] +``` + +--- + +### Nodo 1 — Telegram Trigger +``` +Tipo: Telegram Trigger +Credential: (crear nueva → Token del bot) +Updates: message +``` + +### Nodo 2 — IF: ¿Es audio? +``` +Tipo: IF +Condición: {{ $json.message.voice }} existe + O {{ $json.message.audio }} existe +``` + +### Nodo 3 — HTTP: Descargar audio de Telegram +``` +Tipo: HTTP Request +Método: GET +URL: https://api.telegram.org/bot{{ $env.TELEGRAM_TOKEN }}/getFile +Parámetros: + file_id: {{ $json.message.voice.file_id }} +``` + +Luego un segundo HTTP Request para descargar el archivo: +``` +URL: https://api.telegram.org/file/bot{{ $env.TELEGRAM_TOKEN }}/{{ $json.result.file_path }} +Respuesta: archivo binario +``` + +### Nodo 4 — HTTP: Whisper API (transcripción) +``` +Tipo: HTTP Request +Método: POST +URL: http://100.64.1.10:8765/transcribe ← IP Tailscale de tu PC +Body: form-data + file: [archivo de audio del nodo anterior] +``` + +Resultado esperado: +```json +{ "text": "Reunión con Juan el viernes a las diez de la mañana en el puerto" } +``` + +### Nodo 5 — HTTP: Ollama (extraer evento) +``` +Tipo: HTTP Request +Método: POST +URL: http://100.64.1.10:11434/api/generate +Body JSON: +{ + "model": "qwen2.5:14b", + "stream": false, + "prompt": "Extrae el evento de esta frase y devuelve SOLO JSON válido sin explicaciones:\n\nFrase: '{{ $json.text }}'\n\nFecha actual: {{ $now.format('YYYY-MM-DD') }}, {{ $now.format('dddd') }}\n\nDevuelve exactamente este formato:\n{\"titulo\": \"...\", \"fecha\": \"YYYY-MM-DD\", \"hora_inicio\": \"HH:MM\", \"hora_fin\": \"HH:MM\", \"lugar\": \"...\", \"descripcion\": \"...\"}\n\nSi no hay hora fin, suma 1 hora a la hora inicio. Si no hay lugar, pon vacío." +} +``` + +### Nodo 6 — Code: Parsear respuesta Ollama +```javascript +// Extraer el JSON de la respuesta de Ollama +const responseText = $input.item.json.response; + +// Buscar el JSON dentro del texto (por si Ollama añade texto extra) +const jsonMatch = responseText.match(/\{[\s\S]*\}/); +if (!jsonMatch) throw new Error("Ollama no devolvió JSON válido: " + responseText); + +const evento = JSON.parse(jsonMatch[0]); + +// Construir fechas iCal (formato: 20260704T100000) +const fechaInicio = evento.fecha.replace(/-/g, '') + 'T' + evento.hora_inicio.replace(':', '') + '00'; +const fechaFin = evento.fecha.replace(/-/g, '') + 'T' + evento.hora_fin.replace(':', '') + '00'; + +// UID único para el evento +const uid = 'n8n-' + Date.now() + '@crewinghunters.com'; + +// Formato iCalendar (CalDAV) +const ical = `BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//n8n//Calendario Voz//ES +BEGIN:VEVENT +UID:${uid} +DTSTART:${fechaInicio} +DTEND:${fechaFin} +SUMMARY:${evento.titulo} +LOCATION:${evento.lugar || ''} +DESCRIPTION:${evento.descripcion || 'Creado por voz via n8n'} +END:VEVENT +END:VCALENDAR`; + +return { + json: { + ...evento, + ical, + uid, + fechaInicio, + fechaFin, + // Para el recordatorio: fecha ISO completa + fechaRecordatorio: new Date(evento.fecha + 'T' + evento.hora_inicio + ':00').toISOString() + } +}; +``` + +### Nodo 7 — HTTP: Crear evento en Nextcloud (CalDAV) +``` +Tipo: HTTP Request +Método: PUT +URL: https://tu-nextcloud.com/remote.php/dav/calendars/USUARIO/personal/{{ $json.uid }}.ics +Autenticación: Basic Auth + Usuario: tu_usuario_nextcloud + Password: contraseña_de_aplicacion +Headers: + Content-Type: text/calendar; charset=utf-8 +Body: {{ $json.ical }} +``` + +### Nodo 8 — Telegram: Confirmar +``` +Tipo: Telegram +Operación: Send Message +Chat ID: {{ $env.TELEGRAM_CHAT_ID }} +Texto: +✅ *Evento creado en tu calendario* + +📅 *{{ $json.titulo }}* +🗓 {{ $json.fecha }} +🕐 {{ $json.hora_inicio }} – {{ $json.hora_fin }} +📍 {{ $json.lugar || 'Sin ubicación' }} + +_Te recordaré 30 minutos antes_ ⏰ +``` + +### Nodo 9 — Wait (hasta 30min antes del evento) +``` +Tipo: Wait +Modo: Until specified time +Fecha/Hora: {{ $json.fechaRecordatorio }} +Offset: -30 minutos +``` + +### Nodo 10 — Telegram: Recordatorio +``` +Tipo: Telegram +Operación: Send Message +Chat ID: {{ $env.TELEGRAM_CHAT_ID }} +Texto: +⏰ *Recordatorio — en 30 minutos:* + +📅 *{{ $json.titulo }}* +🕐 {{ $json.hora_inicio }} +📍 {{ $json.lugar || '' }} +``` + +--- + +## PASO 8 — Variables de Entorno en n8n + +En n8n → Settings → Variables → Agregar: + +| Variable | Valor | +|---------|-------| +| `TELEGRAM_TOKEN` | `7123456789:AAF...` (token del bot) | +| `TELEGRAM_CHAT_ID` | `123456789` (tu chat ID) | +| `WHISPER_URL` | `http://100.64.1.10:8765` | +| `OLLAMA_URL` | `http://100.64.1.10:11434` | +| `NEXTCLOUD_URL` | `https://tu-nextcloud.com` | +| `NEXTCLOUD_USER` | `tu_usuario` | +| `NEXTCLOUD_PASS` | `contraseña_de_aplicacion` | + +--- + +## PASO 9 — Prueba Completa + +1. Graba un audio en el teléfono diciendo: + > *"Reunión con Federico el próximo martes a las tres de la tarde en el puerto de Miami"* + +2. Envía el audio al bot de Telegram + +3. En ~10-15 segundos deberías recibir: + ``` + ✅ Evento creado en tu calendario + 📅 Reunión con Federico + 🗓 2026-07-07 + 🕐 15:00 – 16:00 + 📍 Puerto de Miami + ``` + +4. Verifica en Nextcloud Calendar que el evento aparece + +5. Espera el recordatorio 30 min antes ✅ + +--- + +## Checklist de Instalación + +### En tu PC (Windows) +- [ ] Instalar Tailscale → anotar IP (`100.x.x.x`) +- [ ] `pip install faster-whisper fastapi uvicorn python-multipart` +- [ ] Crear `C:\whisper-api\server.py` (código arriba) +- [ ] Crear `C:\whisper-api\start.bat` +- [ ] Agregar `start.bat` al inicio de Windows +- [ ] Iniciar la API y verificar: `curl http://localhost:8765/health` +- [ ] Configurar Ollama para escuchar en `0.0.0.0`: variable `OLLAMA_HOST` + +### En el RPi4 (via SSH) +- [ ] `curl -fsSL https://tailscale.com/install.sh | sh` +- [ ] `sudo tailscale up` → autenticar con la misma cuenta +- [ ] Verificar ping a PC: `tailscale ping 100.x.x.x` +- [ ] Verificar Whisper: `curl http://100.x.x.x:8765/health` +- [ ] Verificar Ollama: `curl http://100.x.x.x:11434/api/tags` + +### En Telegram +- [ ] Crear bot con @BotFather → guardar token +- [ ] Obtener tu Chat ID +- [ ] Escribir un mensaje al bot para activarlo + +### En Nextcloud +- [ ] Copiar URL CalDAV del calendario +- [ ] Crear contraseña de aplicación para n8n + +### En n8n +- [ ] Crear carpetas: Prisa Yachts / Personal / AIS Navigator / Sandbox +- [ ] Crear variables de entorno (8 variables) +- [ ] Crear workflow `🎙️ Voz → Calendario Nextcloud` +- [ ] Agregar los 10 nodos en orden +- [ ] Activar el workflow +- [ ] Prueba con audio real + +--- + +## Solución de Problemas + +| Problema | Causa probable | Solución | +|---------|---------------|---------| +| n8n no llega a Whisper | Tailscale no conectado | `sudo tailscale up` en RPi4 | +| Whisper tarda mucho | Modelo "medium" pesado en CPU | Cambiar a "small" en server.py | +| Ollama no responde | OLLAMA_HOST no configurado | Reiniciar Ollama tras la variable | +| Evento en fecha incorrecta | Ollama interpretó mal el día | Ajustar el prompt con más contexto | +| CalDAV 401 Unauthorized | Contraseña de app incorrecta | Crear nueva contraseña en Nextcloud | +| Audio no se procesa | Formato no soportado | Telegram envía .ogg — faster-whisper lo soporta | + +--- + +## Mejoras Futuras (Fase 2) + +- [ ] **Cancelar eventos por voz**: "Cancela la reunión del martes" → n8n borra el evento +- [ ] **Consultar agenda**: "¿Qué tengo mañana?" → n8n lee el CalDAV y responde +- [ ] **Recordatorio personalizable**: "Recuérdame 1 hora antes" +- [ ] **Múltiples calendarios**: personal, trabajo, AIS Navigator +- [ ] **Texto además de voz**: si escribes (no mandas audio), también funciona + +--- + +*Blueprint generado: 2026-07-03 | AR Electronics / Álvaro*