Files
alro65 235a9abbfe security: SECRET_KEY from env, CORS restricted to localhost
- Replace hardcoded secret_key with os.environ.get('SECRET_KEY')
- RuntimeError if SECRET_KEY not set (fail fast)
- Restrict CORS to localhost:8765 origins (was allow all with credentials)
- Add .gitignore excluding db, env, __pycache__, backups

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 12:55:19 -04:00

1344 lines
73 KiB
HTML

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Boat&Ship-Finder — Global Vessel Intelligence</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,400;0,500&family=Syne:wght@400;600;700;800&display=swap');
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
:root{
--bg:#080c12;--s1:#0e1420;--s2:#141b28;--s3:#1a2235;
--b:rgba(255,255,255,0.07);--b2:rgba(255,255,255,0.14);
--accent:#00b4ff;--green:#00e5a0;--amber:#ffb547;--red:#ff5252;--purple:#a78bfa;
--text:#dde4f0;--muted:#5a6a88;--mono:'DM Mono',monospace;--head:'Syne',system-ui,sans-serif;
}
body{background:var(--bg);color:var(--text);font-family:var(--head);min-height:100vh;overflow-x:hidden}
input,select,button,textarea{font-family:inherit;outline:none}
::-webkit-scrollbar{width:3px;height:3px}
::-webkit-scrollbar-thumb{background:var(--b2);border-radius:2px}
/* Header */
.hdr{background:var(--s1);border-bottom:1px solid var(--b);padding:11px 18px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:200;backdrop-filter:blur(12px)}
.logo{font-weight:800;font-size:16px;letter-spacing:-.03em;white-space:nowrap;display:flex;align-items:center;gap:8px;flex-shrink:0}
.logo-icon{width:30px;height:30px;background:linear-gradient(135deg,var(--accent),var(--green));border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:15px}
.search-wrap{flex:1;position:relative}
.search-in{width:100%;background:var(--s2);border:1px solid var(--b);border-radius:10px;padding:9px 40px 9px 14px;color:var(--text);font-size:13px;transition:.2s}
.search-in:focus{border-color:rgba(0,180,255,.4);background:var(--s3)}
.search-in::placeholder{color:var(--muted)}
.search-go{position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--accent);cursor:pointer;font-size:17px;padding:2px}
.hdr-btns{display:flex;gap:6px;flex-shrink:0}
.hbtn{background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:7px 11px;color:var(--muted);cursor:pointer;font-size:11px;font-weight:700;transition:.2s;white-space:nowrap;letter-spacing:.03em}
.hbtn:hover,.hbtn.on{color:var(--accent);border-color:rgba(0,180,255,.3)}
/* Tabs */
.tabs{background:var(--s1);border-bottom:1px solid var(--b);display:flex;overflow-x:auto;padding:0 16px}
.tab{padding:10px 16px;font-size:12px;font-weight:700;color:var(--muted);border:none;background:none;cursor:pointer;border-bottom:2px solid transparent;transition:.2s;white-space:nowrap;letter-spacing:.04em}
.tab.on{color:var(--accent);border-bottom-color:var(--accent)}
/* Filters */
.fltbar{background:var(--s1);padding:9px 16px;display:flex;gap:7px;flex-wrap:wrap;border-bottom:1px solid var(--b);align-items:center}
.flt{background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:7px 10px;color:var(--text);font-size:12px;cursor:pointer;transition:.2s;height:34px}
.flt:hover{border-color:var(--b2)}
select.flt option{background:var(--s2)}
.flt-price{width:140px}
.flt-year{width:96px}
/* Stats */
.stats{padding:12px 16px;display:flex;gap:8px;overflow-x:auto}
.stat{background:var(--s1);border:1px solid var(--b);border-radius:10px;padding:9px 14px;flex-shrink:0;min-width:100px}
.stat-l{font-size:10px;color:var(--muted);font-weight:700;letter-spacing:.08em;text-transform:uppercase}
.stat-v{font-size:20px;font-weight:700;font-family:var(--mono);margin-top:2px}
/* Grid */
.grid{padding:0 16px 100px;display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px}
/* Card */
.card{background:var(--s1);border:1px solid var(--b);border-radius:14px;overflow:hidden;transition:border-color .2s,transform .1s;display:flex;flex-direction:column}
.card:hover{border-color:rgba(0,180,255,.25);transform:translateY(-1px)}
/* Photo area */
.card-photo{position:relative;height:170px;background:var(--s2);overflow:hidden;flex-shrink:0}
.card-photo img{width:100%;height:100%;object-fit:cover;transition:transform .3s}
.card:hover .card-photo img{transform:scale(1.03)}
.card-photo .no-photo{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;color:var(--muted)}
.card-photo .no-photo .icon{font-size:40px;opacity:.3}
.card-photo .no-photo .lbl{font-size:11px;opacity:.5}
.photo-badges{position:absolute;top:10px;left:10px;display:flex;gap:5px;flex-wrap:wrap}
.photo-score{position:absolute;top:10px;right:10px}
.pbadge{font-size:10px;font-weight:700;padding:3px 9px;border-radius:20px;border:1px solid;backdrop-filter:blur(4px)}
/* Card body */
.card-body{padding:12px 14px;display:flex;flex-direction:column;gap:8px;flex:1}
.card-name{font-size:14px;font-weight:700;line-height:1.3;color:var(--text)}
.card-loc{font-size:11px;color:var(--muted);display:flex;align-items:center;gap:4px}
.specs{display:grid;grid-template-columns:repeat(3,1fr);gap:5px}
.spec{background:var(--s2);border-radius:7px;padding:6px 8px}
.spec-k{font-size:9px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.07em}
.spec-v{font-size:12px;font-weight:500;font-family:var(--mono);margin-top:2px;color:var(--text)}
.desc{font-size:11px;color:var(--muted);line-height:1.6;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.flags{display:flex;flex-wrap:wrap;gap:4px}
.flag{font-size:10px;padding:2px 8px;border-radius:20px;background:rgba(0,180,255,.1);border:1px solid rgba(0,180,255,.2);color:var(--accent)}
/* Card footer */
.card-foot{display:flex;justify-content:space-between;align-items:center;border-top:1px solid var(--b);padding-top:10px;margin-top:auto}
.price{font-size:18px;font-weight:700;font-family:var(--mono)}
.card-btns{display:flex;gap:5px}
.cbtn{padding:6px 11px;border-radius:8px;font-size:11px;font-weight:700;border:1px solid var(--b);background:transparent;color:var(--muted);cursor:pointer;transition:.2s;letter-spacing:.03em}
.cbtn:hover{border-color:var(--b2);color:var(--text)}
.cbtn.saved{background:rgba(0,229,160,.1);border-color:rgba(0,229,160,.3);color:var(--green)}
.cbtn.primary{background:rgba(0,180,255,.12);border-color:rgba(0,180,255,.3);color:var(--accent)}
/* Score ring */
.ring{position:relative;width:44px;height:44px;flex-shrink:0}
.ring svg{position:absolute;top:0;left:0}
.ring-n{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;font-family:var(--mono)}
/* Panels */
.panel{position:fixed;inset:0;z-index:500;display:none;align-items:flex-end;background:rgba(0,0,0,.75);backdrop-filter:blur(6px)}
.panel.show{display:flex}
.pbox{background:var(--s1);border-top:1px solid var(--b);border-radius:20px 20px 0 0;padding:22px 18px;width:100%;max-width:600px;margin:0 auto;max-height:92vh;overflow-y:auto}
.ptitle{font-size:15px;font-weight:800;margin-bottom:14px;display:flex;justify-content:space-between;align-items:center}
.pclose{background:none;border:none;color:var(--muted);cursor:pointer;font-size:20px;line-height:1;padding:0 2px}
.field{margin-bottom:12px}
.field label{display:block;font-size:11px;font-weight:700;color:var(--muted);letter-spacing:.07em;text-transform:uppercase;margin-bottom:5px}
.field input,.field select,.field textarea{width:100%;background:var(--s2);border:1px solid var(--b);border-radius:9px;padding:9px 12px;color:var(--text);font-size:13px;transition:.2s}
.field input:focus,.field select:focus,.field textarea:focus{border-color:rgba(0,180,255,.4)}
.field textarea{min-height:80px;resize:vertical}
.btn-full{width:100%;background:var(--accent);color:#000;border:none;border-radius:10px;padding:12px;font-size:13px;font-weight:800;cursor:pointer;margin-top:6px;transition:.15s;letter-spacing:.02em}
.btn-full:hover{opacity:.9}
.btn-full:active{transform:scale(.98)}
.btn-sec{background:var(--s2);border:1px solid var(--b);color:var(--text)}
.fg2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
/* Status bar */
.sbar{position:fixed;bottom:0;left:0;right:0;background:var(--s1);border-top:1px solid var(--b);padding:7px 18px;display:flex;align-items:center;justify-content:space-between;z-index:100;font-size:11px;color:var(--muted)}
.sdot{width:7px;height:7px;border-radius:50%;background:var(--green);flex-shrink:0}
.sdot.off{background:var(--red)}
@keyframes pulse{0%,100%{opacity:.5}50%{opacity:1}}
.spin{width:30px;height:30px;border:2px solid var(--b2);border-top-color:var(--accent);border-radius:50%;animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
/* Toast */
.toast{position:fixed;bottom:72px;left:50%;transform:translateX(-50%);background:var(--s3);border:1px solid var(--b2);border-radius:10px;padding:9px 18px;font-size:13px;font-weight:600;z-index:999;opacity:0;transition:opacity .25s;pointer-events:none;white-space:nowrap}
.toast.show{opacity:1}
/* Empty */
.empty{text-align:center;padding:50px 20px;color:var(--muted)}
.empty-icon{font-size:44px;margin-bottom:12px;opacity:.4}
/* Search progress */
.srch-prog{background:var(--s2);border:1px solid var(--b);border-radius:12px;padding:20px;text-align:center;margin-top:14px}
.srch-prog .spin{margin:0 auto 12px}
.srch-log{max-height:120px;overflow-y:auto;text-align:left;font-size:11px;color:var(--muted);font-family:var(--mono);margin-top:10px;padding:8px;background:var(--bg);border-radius:8px}
/* Table */
.twrap{padding:0 16px 100px;overflow-x:auto}
table{width:100%;border-collapse:collapse;min-width:900px}
th{padding:9px 12px;text-align:left;font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;border-bottom:1px solid var(--b);white-space:nowrap}
td{padding:9px 12px;font-size:12px;border-bottom:1px solid rgba(255,255,255,.03)}
tr:hover td{background:rgba(255,255,255,.02)}
/* Sources grid */
.src-cat{margin-bottom:18px}
.src-cat-title{font-size:11px;font-weight:700;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;margin-bottom:8px}
.src-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:6px}
.src-item{background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:7px 10px;display:flex;justify-content:space-between;align-items:center}
.src-name{font-size:11px;font-weight:600}
.src-type{font-size:10px;color:var(--muted)}
@media(min-width:640px){
.grid{grid-template-columns:repeat(2,1fr)}
.hdr{padding:12px 24px}
.stats{padding:12px 24px}
.fltbar{padding:9px 24px}
.tabs{padding:0 24px}
.grid{padding:0 24px 100px}
}
@media(min-width:1100px){.grid{grid-template-columns:repeat(3,1fr)}}
@media(min-width:1400px){.grid{grid-template-columns:repeat(4,1fr)}}
.login-overlay{position:fixed;inset:0;background:var(--bg);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px}
.login-box{background:var(--s1);border:1px solid var(--b2);border-radius:16px;padding:32px;width:100%;max-width:360px}
.login-logo{text-align:center;margin-bottom:24px}
.login-logo .icon{font-size:36px;margin-bottom:8px}
.login-logo .name{font-size:20px;font-weight:800;letter-spacing:-.02em}
.login-err{background:rgba(255,82,82,.1);border:1px solid rgba(255,82,82,.3);border-radius:8px;padding:9px 12px;font-size:12px;color:var(--red);margin-bottom:12px;display:none}
</style>
</head>
<body>
<!-- Header -->
<div class="hdr">
<div class="logo">
<div class="logo-icon"></div>
<span><span style="color:var(--accent)">Boat</span><span style="color:var(--muted);font-weight:400">&</span><span style="color:var(--green)">Ship</span><span style="color:var(--text);font-weight:400">-Finder</span></span>
</div>
<div class="search-wrap">
<input class="search-in" id="mainSearch" placeholder="Buscar: tug acero subasta, offshore vessel Florida, pesquero noruego…" onkeydown="if(event.key==='Enter')doSearch()">
<button class="search-go" onclick="doSearch()"></button>
</div>
<div class="hdr-btns">
<span id="headerUser" style="font-size:11px;color:var(--muted);display:flex;align-items:center;gap:6px"></span>
<button class="hbtn" onclick="doLogout()" title="Cerrar sesión">⎋ Salir</button>
<button class="hbtn" onclick="openPanel('analyze')">📋 Analizar</button>
<button class="hbtn" onclick="openPanel('add')">+ Agregar</button>
<button class="hbtn" onclick="openPanel('sources')">🌐 Fuentes</button>
<button class="hbtn" id="viewBtn" onclick="toggleView()">☰ Tabla</button>
</div>
</div>
<!-- Tabs -->
<div class="tabs">
<button class="tab on" onclick="switchTab('all',this)">🌊 Todas</button>
<button class="tab" onclick="switchTab('saved',this)">★ Guardadas <span id="savedCount" style="background:rgba(0,229,160,.2);color:var(--green);font-size:10px;padding:1px 6px;border-radius:10px;margin-left:4px;display:none">0</span></button>
<button class="tab" onclick="switchTab('auction',this)">🔨 Subastas</button>
<button class="tab" onclick="switchTab('salvage',this)">🔧 Salvage</button>
<button class="tab" onclick="switchTab('alerts',this)">🔔 Alertas</button>
<button class="tab" onclick="switchTab('history',this)">🕐 Historial</button>
<button class="tab" onclick="switchTab('collections',this)">📁 Carpetas</button>
</div>
<!-- Selection toolbar -->
<div id="selBar" style="display:none;background:rgba(0,180,255,.1);border-bottom:1px solid rgba(0,180,255,.25);padding:9px 18px;display:none;align-items:center;gap:12px;flex-wrap:wrap">
<span style="font-size:13px;font-weight:700;color:var(--accent)" id="selCount">0 seleccionadas</span>
<button onclick="saveSelectedToCollection()" style="background:var(--accent);color:#000;border:none;border-radius:8px;padding:7px 16px;font-size:12px;font-weight:800;cursor:pointer">
📁 Guardar en carpeta
</button>
<button onclick="clearSelection()" style="background:none;border:1px solid var(--b2);border-radius:8px;padding:7px 12px;font-size:12px;color:var(--muted);cursor:pointer">
Cancelar selección
</button>
<span style="font-size:11px;color:var(--muted)">Clic en ☐ de cada tarjeta para seleccionar</span>
</div>
<!-- Filters -->
<div class="fltbar" id="fltbar">
<select class="flt" onchange="state.f.type=this.value;loadVessels()">
<option value="">Todos los tipos</option>
<option>Yacht</option><option>Motor</option><option>Sailboat</option>
<option>Fishing</option><option>Tug</option><option>Barge</option>
<option>Offshore</option><option>Ferry</option><option>Other</option>
</select>
<select class="flt" onchange="state.f.status=this.value;loadVessels()">
<option value="">Todos los estados</option>
<option value="active">Activo</option>
<option value="auction">Subasta</option>
<option value="salvage">Salvage</option>
<option value="abandoned">Abandonado</option>
</select>
<select class="flt" onchange="state.f.hull=this.value;loadVessels()">
<option value="">Cualquier casco</option>
<option>Fiberglass</option><option>Steel</option>
<option>Aluminum</option><option>Wood</option><option>Composite</option>
</select>
<input class="flt flt-price" type="number" placeholder="Precio máx. USD" min="0"
oninput="state.f.max_price=this.value;loadVessels()">
<input class="flt flt-price" type="number" placeholder="LOA mín. (m)" min="0"
oninput="state.f.min_loa=this.value;loadVessels()">
<input class="flt flt-year" type="number" placeholder="Año desde" min="1900" max="2026"
oninput="state.f.year_min=this.value;loadVessels()">
<input class="flt flt-year" type="number" placeholder="Año hasta" min="1900" max="2026"
oninput="state.f.year_max=this.value;loadVessels()">
<select class="flt" onchange="state.f.sort=this.value;loadVessels()">
<option value="score">↓ Mejor oportunidad</option>
<option value="price_asc">$ Precio ↑</option>
<option value="price_desc">$ Precio ↓</option>
<option value="loa">LOA ↓</option>
<option value="year">Año ↓</option>
<option value="newest">Más recientes</option>
</select>
</div>
<!-- Stats -->
<div class="stats">
<div class="stat"><div class="stat-l">Total</div><div class="stat-v" id="sTotal"></div></div>
<div class="stat"><div class="stat-l">Score prom.</div><div class="stat-v" id="sScore" style="color:var(--green)"></div></div>
<div class="stat"><div class="stat-l">Precio prom.</div><div class="stat-v" id="sPrice" style="font-size:14px"></div></div>
<div class="stat"><div class="stat-l">Subastas</div><div class="stat-v" id="sAuct" style="color:var(--amber)"></div></div>
<div class="stat"><div class="stat-l">Salvage</div><div class="stat-v" id="sSalv" style="color:var(--red)"></div></div>
<div class="stat"><div class="stat-l">Guardadas</div><div class="stat-v" id="sSaved" style="color:var(--accent)"></div></div>
</div>
<!-- Content -->
<div id="content"></div>
<!-- Status bar -->
<div class="sbar">
<div style="display:flex;align-items:center;gap:8px">
<div class="sdot" id="sdot"></div>
<span id="sstat">Conectando…</span>
</div>
<div style="display:flex;gap:14px">
<span id="smodel"></span>
<span id="ssrc"></span>
</div>
</div>
<div class="toast" id="toast"></div>
<!-- Panel: Search -->
<div class="panel" id="pSearch" onclick="if(event.target===this)closePanel('search')">
<div class="pbox">
<div class="ptitle">⚡ Búsqueda Global Real-Time <button class="pclose" onclick="closePanel('search')"></button></div>
<p style="font-size:12px;color:var(--muted);margin-bottom:14px;line-height:1.6">
Busca en <strong style="color:var(--text)">todo internet simultáneamente</strong> — subastas, clasificados, astilleros, periódicos, revistas náuticas, registros de buques y más. La IA extrae datos reales de cada resultado.
</p>
<div class="field"><label>¿Qué buscas?</label>
<input id="aiQ" placeholder="Ej: offshore support vessel steel 50m+ auction Gulf, pesquero acero salvage, tug harbor diesel 2000HP…">
</div>
<div class="fg2">
<div class="field"><label>Tipo de embarcación</label>
<select id="aiType"><option value="">Todos</option><option>Yacht</option><option>Motor</option><option>Sailboat</option><option>Fishing</option><option>Tug</option><option>Barge</option><option>Offshore</option><option>Ferry</option></select>
</div>
<div class="field"><label>Estado preferido</label>
<select id="aiStat"><option value="">Cualquiera</option><option value="active">Activo</option><option value="auction">Subasta</option><option value="salvage">Salvage</option></select>
</div>
<div class="field"><label>Precio máx. (USD)</label>
<input id="aiPrice" type="number" placeholder="Sin límite">
</div>
<div class="field"><label>LOA mín. (metros)</label>
<input id="aiLoa" type="number" placeholder="Ej: 10">
</div>
<div class="field"><label>LOA máx. (metros)</label>
<input id="aiLoaMax" type="number" placeholder="Sin límite">
</div>
<div class="field"><label>Año mín.</label>
<input id="aiYearMin" type="number" placeholder="Ej: 1990" min="1900" max="2025">
</div>
<div class="field"><label>Año máx.</label>
<input id="aiYearMax" type="number" placeholder="Ej: 2010" min="1900" max="2025">
</div>
</div>
<div class="field"><label>Región / País</label>
<input id="aiRegion" placeholder="USA, Europa, Caribe, Asia, Global…">
</div>
<div class="field"><label>Notas adicionales</label>
<textarea id="aiNotes" placeholder="Material, uso previsto, condición, historial…"></textarea>
</div>
<button class="btn-full" id="srchBtn" onclick="runSearch()">⚡ Buscar en Todo Internet</button>
<div id="srchResult"></div>
</div>
</div>
<!-- Panel: Analyze -->
<div class="panel" id="pAnalyze" onclick="if(event.target===this)closePanel('analyze')">
<div class="pbox">
<div class="ptitle">📋 Analizar Anuncio <button class="pclose" onclick="closePanel('analyze')"></button></div>
<p style="font-size:12px;color:var(--muted);margin-bottom:14px">Pega cualquier texto — anuncio de periódico, email, clasificado, descripción de subasta. La IA extrae todos los datos técnicos.</p>
<div class="field"><label>Fuente (opcional)</label>
<input id="aSrc" placeholder="Nombre del sitio, periódico, contacto…">
</div>
<div class="field"><label>Texto del anuncio</label>
<textarea id="aTxt" style="min-height:160px" placeholder="Pega aquí el texto completo…"></textarea>
</div>
<button class="btn-full" id="aBtn" onclick="runAnalyze()">🤖 Extraer con IA</button>
<div id="aResult"></div>
</div>
</div>
<!-- Panel: Add -->
<div class="panel" id="pAdd" onclick="if(event.target===this)closePanel('add')">
<div class="pbox">
<div class="ptitle">+ Agregar Embarcación <button class="pclose" onclick="closePanel('add')"></button></div>
<div class="fg2">
<div class="field" style="grid-column:1/-1"><label>Nombre</label><input id="nName" placeholder="Nombre del barco"></div>
<div class="field"><label>Tipo</label>
<select id="nType"><option>Yacht</option><option>Motor</option><option>Sailboat</option><option>Fishing</option><option>Tug</option><option>Barge</option><option>Offshore</option><option>Other</option></select>
</div>
<div class="field"><label>Estado</label>
<select id="nStat"><option value="active">Activo</option><option value="auction">Subasta</option><option value="salvage">Salvage</option><option value="abandoned">Abandonado</option></select>
</div>
<div class="field"><label>LOA (metros)</label><input id="nLoa" type="number" step="0.1" placeholder="0.0"></div>
<div class="field"><label>Manga (metros)</label><input id="nBeam" type="number" step="0.1" placeholder="0.0"></div>
<div class="field"><label>Calado (metros)</label><input id="nDraft" type="number" step="0.1" placeholder="0.0"></div>
<div class="field"><label>Año</label><input id="nYear" type="number" placeholder="2000"></div>
<div class="field"><label>Casco</label>
<select id="nHull"><option>Fiberglass</option><option>Steel</option><option>Aluminum</option><option>Wood</option><option>Composite</option><option>Unknown</option></select>
</div>
<div class="field"><label>Propulsión</label>
<select id="nProp"><option>Diesel</option><option>Gasoline</option><option>Electric</option><option>Sail</option><option>None</option><option>Unknown</option></select>
</div>
<div class="field"><label>Precio (USD)</label><input id="nPrice" type="number" placeholder="Precio"></div>
<div class="field" style="grid-column:1/-1"><label>Ubicación</label><input id="nLoc" placeholder="Ciudad, País"></div>
<div class="field" style="grid-column:1/-1"><label>URL del anuncio</label><input id="nUrl" placeholder="https://…"></div>
<div class="field" style="grid-column:1/-1"><label>Foto (URL de imagen)</label><input id="nImg" placeholder="https://imagen.jpg"></div>
<div class="field" style="grid-column:1/-1"><label>Fuente</label><input id="nSrc2" placeholder="YachtWorld, Craigslist, Contacto directo…"></div>
<div class="field" style="grid-column:1/-1"><label>Descripción</label><textarea id="nDesc" placeholder="Condición, historia, notas…"></textarea></div>
</div>
<button class="btn-full" onclick="addVessel()">Guardar Embarcación</button>
</div>
</div>
<!-- Panel: Sources -->
<div class="panel" id="pSources" onclick="if(event.target===this)closePanel('sources')">
<div class="pbox">
<div class="ptitle">🌐 Fuentes Globales <button class="pclose" onclick="closePanel('sources')"></button></div>
<!-- Facebook Marketplace setup -->
<div id="fbSetupBox" style="background:rgba(24,119,242,.1);border:1px solid rgba(24,119,242,.35);border-radius:12px;padding:14px;margin-bottom:16px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px">
<span style="font-size:20px">📘</span>
<div>
<div style="font-size:12px;font-weight:700;color:#4a9eff">Facebook Marketplace</div>
<div id="fbStatus" style="font-size:11px;color:var(--muted)">Verificando sesión…</div>
</div>
<button id="fbSetupBtn" onclick="setupFacebook()" style="margin-left:auto;background:rgba(24,119,242,.2);border:1px solid rgba(24,119,242,.5);border-radius:8px;padding:6px 14px;color:#4a9eff;font-size:11px;font-weight:700;cursor:pointer;font-family:inherit;white-space:nowrap">
Configurar FB
</button>
</div>
<div style="font-size:10px;color:var(--muted);line-height:1.6">
⚠ Usa una <strong>cuenta secundaria</strong>, no tu cuenta personal. Al hacer clic, se abrirá un navegador para que inicies sesión manualmente — solo se hace una vez.
</div>
</div>
<!-- Add source form -->
<div style="background:var(--s2);border:1px solid var(--b);border-radius:12px;padding:14px;margin-bottom:16px">
<div style="font-size:12px;font-weight:700;color:var(--accent);margin-bottom:10px">+ Agregar nueva fuente</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">
<input id="newSrcName" placeholder="Nombre ej: SailNL" style="background:var(--s1);border:1px solid var(--b);border-radius:8px;padding:7px 10px;color:var(--text);font-size:12px">
<select id="newSrcType" style="background:var(--s1);border:1px solid var(--b);border-radius:8px;padding:7px 10px;color:var(--text);font-size:12px">
<option value="broker">Broker / Especializado</option>
<option value="classifieds">Clasificados</option>
<option value="auction">Subasta</option>
<option value="salvage">Salvage</option>
<option value="commercial">Comercial</option>
<option value="news">Noticias</option>
</select>
</div>
<div style="margin-bottom:8px">
<input id="newSrcUrl" placeholder="URL: https://sitio.com/buscar (se agrega ?q= automáticamente)" style="width:100%;background:var(--s1);border:1px solid var(--b);border-radius:8px;padding:7px 10px;color:var(--text);font-size:12px">
</div>
<div style="display:grid;grid-template-columns:1fr auto;gap:8px">
<input id="newSrcCat" placeholder="Categoría ej: Veleros Holanda" style="background:var(--s1);border:1px solid var(--b);border-radius:8px;padding:7px 10px;color:var(--text);font-size:12px">
<button onclick="addCustomSource()" style="background:var(--accent);color:#000;border:none;border-radius:8px;padding:7px 16px;font-size:12px;font-weight:800;cursor:pointer;white-space:nowrap">Agregar ✓</button>
</div>
<div style="font-size:10px;color:var(--muted);margin-top:6px">
💡 Si la URL ya tiene búsqueda: pega la URL completa con <code style="color:var(--accent)">{query}</code> donde va el texto.<br>
Ejemplo: <code style="color:var(--muted)">https://www.sitio.com/buscar?texto={query}&tipo=barco</code>
</div>
</div>
<div id="srcList"></div>
</div>
</div>
<!-- Panel: Alert -->
<div class="panel" id="pAlert" onclick="if(event.target===this)closePanel('alert')">
<div class="pbox">
<div class="ptitle">🔔 Nueva Alerta <button class="pclose" onclick="closePanel('alert')"></button></div>
<div class="field"><label>Nombre</label><input id="alName" placeholder="Ej: Tugs baratos Florida"></div>
<div class="fg2">
<div class="field"><label>Tipo</label>
<select id="alType"><option value="">Todos</option><option>Yacht</option><option>Fishing</option><option>Tug</option><option>Barge</option><option>Offshore</option></select>
</div>
<div class="field"><label>Estado</label>
<select id="alStat"><option value="">Cualquiera</option><option value="auction">Subasta</option><option value="salvage">Salvage</option></select>
</div>
<div class="field"><label>Precio máx. USD</label><input id="alPrice" type="number" placeholder="Sin límite"></div>
<div class="field"><label>LOA mín. (m)</label><input id="alLoa" type="number" placeholder="Cualquiera"></div>
</div>
<button class="btn-full" onclick="createAlert()">Crear Alerta</button>
</div>
</div>
<script>
const $ = id => document.getElementById(id);
// ── Auth ───────────────────────────────────────────────────────────────────
let currentUser = null;
async function checkAuth(){
try{
const r = await fetch('/api/me', {credentials:'include'});
if(r.ok){
const d = await r.json();
if(d.logged_in){
currentUser = d;
showApp();
return;
}
}
} catch(e){}
showLogin();
}
function showLogin(){
$('loginOverlay').style.display = 'flex';
$('mainApp').style.display = 'none';
setTimeout(()=>$('loginUser').focus(), 100);
}
function showApp(){
$('loginOverlay').style.display = 'none';
$('mainApp').style.display = 'block';
// Update user display in header
const userEl = $('headerUser');
if(userEl && currentUser) userEl.textContent = '👤 ' + currentUser.username;
checkStatus();
loadVessels();
}
async function doLogin(){
const username = $('loginUser').value.trim();
const password = $('loginPass').value;
if(!username || !password){ $('loginErr').style.display='block'; $('loginErr').textContent='Completa usuario y contraseña'; return; }
$('loginBtn').textContent = 'Entrando…';
$('loginBtn').disabled = true;
try{
const r = await fetch('/api/login', {
method:'POST', credentials:'include',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({username, password})
});
const d = await r.json();
if(d.ok){
currentUser = d;
$('loginErr').style.display = 'none';
showApp();
} else {
$('loginErr').style.display = 'block';
$('loginErr').textContent = d.error || 'Error de autenticación';
}
} catch(e){
$('loginErr').style.display = 'block';
$('loginErr').textContent = 'Error de conexión con el servidor';
}
$('loginBtn').textContent = 'Entrar →';
$('loginBtn').disabled = false;
}
async function doLogout(){
await fetch('/api/logout', {method:'POST', credentials:'include'});
currentUser = null;
showLogin();
}
const API = '';
const FLAG_L = {
below_market:'💰 Precio bajo', rare:'⭐ Raro',
salvage_value:'🔧 Valor salvage', motivated_seller:'🔥 Urgente',
auction:'🔨 Subasta', commercial:'🏭 Comercial',
government_surplus:'🏛 Excedente gobierno'
};
const SC = {active:'#00e5a0',auction:'#ffb547',salvage:'#ff5252',abandoned:'#5a6a88',sold:'#5a6a88'};
const SL = {active:'Activo',auction:'Subasta',salvage:'Salvage',abandoned:'Abandonado',sold:'Vendido'};
const TI = {Yacht:'⚓',Fishing:'🎣',Sailboat:'⛵',Tug:'🚢',Motor:'🛥️',Barge:'🔵',Offshore:'🔧',Ferry:'⛴️',Other:'🚤'};
// Placeholder images by vessel type
const TYPE_IMG = {
Yacht:'https://images.unsplash.com/photo-1567899378494-47b22a2ae96a?w=600&q=80',
Sailboat:'https://images.unsplash.com/photo-1506197603052-3cc9c3a201bd?w=600&q=80',
Fishing:'https://images.unsplash.com/photo-1511884642898-4c92249e20b6?w=600&q=80',
Tug:'https://images.unsplash.com/photo-1568430462989-44163eb1752f?w=600&q=80',
Barge:'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=600&q=80',
Offshore:'https://images.unsplash.com/photo-1504309092620-4d0ec726efa4?w=600&q=80',
Motor:'https://images.unsplash.com/photo-1520637836993-a081443f2a56?w=600&q=80',
Ferry:'https://images.unsplash.com/photo-1534889156165-d5de8e545c9d?w=600&q=80',
Other:'https://images.unsplash.com/photo-1493770348161-369560ae357d?w=600&q=80',
};
const state = {
tab:'all', view:'grid', vessels:[], savedIds:new Set(),
selected:new Set(), collections:[], activeCollection:null,
f:{type:'',status:'',hull:'',max_price:'',min_loa:'',year_min:'',year_max:'',sort:'score'}
};
// Score helpers
function sc(s){return s>=80?'var(--green)':s>=60?'var(--amber)':'var(--muted)'}
function ring(s){
const c=sc(s),r=19,ci=2*Math.PI*r,d=(s/100)*ci;
return `<div class="ring"><svg width="44" height="44" viewBox="0 0 44 44">
<circle cx="22" cy="22" r="${r}" fill="none" stroke="rgba(255,255,255,.07)" stroke-width="2.5"/>
<circle cx="22" cy="22" r="${r}" fill="none" stroke="${c}" stroke-width="2.5"
stroke-dasharray="${d} ${ci}" stroke-dashoffset="${ci/4}" stroke-linecap="round"/>
</svg><div class="ring-n" style="color:${c}">${Math.round(s)}</div></div>`;
}
// Card renderer
function card(v){
const stc=SC[v.status]||'#5a6a88';
const saved=state.savedIds.has(v.id);
const flags=(v.flags||[]).filter(f=>FLAG_L[f]);
const price=v.price_usd?'$'+Math.round(v.price_usd).toLocaleString():'Consultar';
const pc=(v.score||0)>=80?'var(--green)':'var(--text)';
// Photo: use first image from array, or type placeholder
const imgs=v.images||[];
const _PROXY_HOSTS=['sailboatlistings.com','yachtworld.com','boattrader.com',
'apolloduck.com','rightboat.com','boat24.com','seaboats.net','boats.com'];
function proxyImg(u){
if(!u||!u.startsWith('http')) return u;
try{ const h=new URL(u).hostname; if(_PROXY_HOSTS.some(d=>h.includes(d))) return '/api/img_proxy?url='+encodeURIComponent(u); } catch(e){}
return u;
}
let imgSrc = imgs.length ? proxyImg(imgs[0]) : (TYPE_IMG[v.vessel_type]||TYPE_IMG.Other);
// If stored image looks like a real URL use it, else use placeholder
const hasRealImg = imgs.length > 0 && imgs[0].startsWith('http');
return `<div class="card">
<div class="card-photo" onclick="toggleSelect(${v.id},event)" style="cursor:pointer">
<img src="${imgSrc}" alt="${v.name||''}" loading="lazy"
onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
<div class="no-photo" style="display:none">
<div class="icon">${TI[v.vessel_type]||'🚢'}</div>
<div class="lbl">${v.vessel_type||'Vessel'}</div>
</div>
<div class="photo-badges">
<span class="pbadge" style="color:${stc};border-color:${stc}40;background:rgba(0,0,0,.55)">${SL[v.status]||v.status}</span>
${flags.slice(0,2).map(f=>`<span class="pbadge" style="color:var(--accent);border-color:rgba(0,180,255,.3);background:rgba(0,0,0,.55)">${FLAG_L[f]}</span>`).join('')}
</div>
<div class="photo-score">${ring(v.score||0)}</div>
<div id="chk-${v.id}" style="position:absolute;bottom:10px;left:10px;width:24px;height:24px;border-radius:6px;border:2px solid rgba(255,255,255,.7);background:${state.selected.has(v.id)?'var(--accent)':'rgba(0,0,0,.4)'};display:flex;align-items:center;justify-content:center;font-size:13px;transition:.15s">
${state.selected.has(v.id)?'✓':''}
</div>
</div>
<div class="card-body">
<div>
<div style="display:flex;align-items:center;gap:5px;margin-bottom:4px;flex-wrap:wrap">
<span style="font-size:11px;background:var(--s2);padding:2px 8px;border-radius:6px;color:var(--muted);font-weight:700">${v.vessel_type||'?'}</span>
${v.source_name?`<span style="font-size:10px;font-weight:600;padding:2px 7px;border-radius:5px;background:rgba(0,180,255,.10);color:var(--accent);border:1px solid rgba(0,180,255,.22);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:130px" title="${v.source_name}">🔗 ${v.source_name}</span>`:''}
</div>
<div class="card-name">${v.name||'Sin nombre'}</div>
<div class="card-loc">📍 ${v.location||'—'}${v.country?' ('+v.country+')':''}</div>
</div>
<div class="specs">
<div class="spec"><div class="spec-k">LOA</div><div class="spec-v">${v.loa_m?v.loa_m+'m':'—'}</div></div>
<div class="spec"><div class="spec-k">Manga</div><div class="spec-v">${v.beam_m?v.beam_m+'m':'—'}</div></div>
<div class="spec"><div class="spec-k">Calado</div><div class="spec-v">${v.draft_m?v.draft_m+'m':'—'}</div></div>
<div class="spec"><div class="spec-k">Año</div><div class="spec-v">${v.year_built||'—'}</div></div>
<div class="spec"><div class="spec-k">Casco</div><div class="spec-v">${v.hull||'—'}</div></div>
<div class="spec"><div class="spec-k">Motor</div><div class="spec-v">${v.propulsion||'—'}</div></div>
</div>
${v.description?`<div class="desc">${v.description}</div>`:''}
${flags.length?`<div class="flags">${flags.map(f=>`<span class="flag">${FLAG_L[f]}</span>`).join('')}</div>`:''}
<div class="card-foot">
<div>
<span class="price" style="color:${pc}">${price}</span>
${v.currency&&v.currency!=='USD'?`<span style="font-size:10px;color:var(--muted);margin-left:3px">${v.currency}</span>`:''}
</div>
<div class="card-btns">
<button class="cbtn ${saved?'saved':''}" onclick="toggleSave(${v.id})" style="${saved?'':'border-color:rgba(0,229,160,.3);color:var(--green)'}">${saved?'★ Guardada':'☆ Guardar'}</button>
${v.source_url?`<button class="cbtn primary" onclick="openSrc(${v.id})">Ver →</button>`:''}
</div>
</div>
</div>
</div>`;
}
function renderGrid(v){
$('content').innerHTML=`<div class="grid">${v.map(card).join('')}</div>`;
}
function renderTable(vessels){
$('content').innerHTML=`<div class="twrap"><table>
<thead><tr><th>Score</th><th>Foto</th><th>Nombre</th><th>Tipo</th><th>Estado</th><th>LOA</th><th>Año</th><th>Precio</th><th>Ubicación</th><th>Fuente</th><th></th></tr></thead>
<tbody>${vessels.map(v=>{
const imgs=v.images||[];
const imgSrc=imgs.length?imgs[0]:(TYPE_IMG[v.vessel_type]||TYPE_IMG.Other);
const saved=state.savedIds.has(v.id);
return `<tr>
<td style="font-family:var(--mono);font-weight:700;color:${sc(v.score||0)}">${Math.round(v.score||0)}</td>
<td><img src="${imgSrc}" style="width:48px;height:36px;object-fit:cover;border-radius:5px" onerror="this.style.display='none'"></td>
<td style="font-weight:600;max-width:180px">${v.name||'—'}</td>
<td style="color:var(--muted)">${v.vessel_type||'—'}</td>
<td><span style="color:${SC[v.status]||'#5a6a88'};font-size:11px;font-weight:600">${SL[v.status]||v.status}</span></td>
<td style="font-family:var(--mono)">${v.loa_m?v.loa_m+'m':'—'}</td>
<td style="color:var(--muted)">${v.year_built||'—'}</td>
<td style="font-family:var(--mono);font-weight:600;color:${(v.score||0)>=80?'var(--green)':'var(--text)'}">${v.price_usd?'$'+Math.round(v.price_usd).toLocaleString():'—'}</td>
<td style="color:var(--muted);max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${v.location||'—'}</td>
<td style="color:var(--accent);font-size:11px">${v.source_name||'—'}</td>
<td><button class="cbtn ${saved?'saved':''}" onclick="toggleSave(${v.id})">${saved?'★':'☆'}</button></td>
</tr>`;
}).join('')}</tbody>
</table></div>`;
}
// Status & data
async function checkStatus(){
try{
const r=await fetch('/api/status');const d=await r.json();
$('sdot').classList.remove('off');
$('sstat').textContent=`Conectado · ${d.db_counts?.vessels||0} embarcaciones`;
$('smodel').textContent=`IA: ${d.active_model||'—'}`;
$('ssrc').textContent=`Fuentes: ${d.sources_count||0}`;
loadStats();
}catch(e){
$('sdot').classList.add('off');
$('sstat').textContent='Servidor desconectado — inicia INICIAR.bat';
}
}
async function loadStats(){
try{
const r=await fetch('/api/stats');const d=await r.json();
$('sTotal').textContent=d.total||0;
$('sScore').textContent=Math.round(d.avg_score||0);
$('sPrice').textContent=d.avg_price?'$'+(Math.round(d.avg_price/1000))+'k':'—';
$('sAuct').textContent=(d.by_status?.auction)||0;
$('sSalv').textContent=(d.by_status?.salvage)||0;
$('sSaved').textContent=d.saved||0;
const sc=$('savedCount');
if(sc){if(d.saved>0){sc.textContent=d.saved;sc.style.display='inline';}else sc.style.display='none';}
}catch(e){}
}
async function loadVessels(){
$('content').innerHTML=`<div class="empty"><div class="spin" style="margin:0 auto"></div></div>`;
try{
let data;
if(state.tab==='saved'){
const r=await fetch('/api/saved');data=await r.json();
} else {
const p=new URLSearchParams();
const f=state.f;
if(f.type) p.set('type',f.type);
if(f.hull) p.set('hull',f.hull);
if(f.max_price) p.set('max_price',f.max_price);
if(f.min_loa) p.set('min_loa',f.min_loa);
if(f.year_min) p.set('year_min',f.year_min);
if(f.year_max) p.set('year_max',f.year_max);
if(f.sort) p.set('sort',f.sort);
if(state.tab==='auction') p.set('status','auction');
else if(state.tab==='salvage') p.set('status','salvage');
else if(f.status) p.set('status',f.status);
const r=await fetch('/api/vessels?'+p.toString());data=await r.json();
}
state.vessels=data.vessels||[];
const sr=await fetch('/api/saved');const sd=await sr.json();
state.savedIds=new Set((sd.vessels||[]).map(v=>v.id));
renderContent();
}catch(e){
$('content').innerHTML=`<div class="empty"><div class="empty-icon">⚠️</div><div>No se pudo conectar al servidor</div><div style="font-size:12px;margin-top:6px;color:var(--muted)">Asegúrate que server.py está corriendo</div></div>`;
}
}
function renderContent(){
const v=state.vessels;
if(!v.length){
if($('liveProgress')) return;
if(state.tab==='saved'){
$('content').innerHTML=`<div class="empty">
<div class="empty-icon">★</div>
<div style="font-size:15px;font-weight:600;margin-bottom:8px">No tienes embarcaciones guardadas</div>
<div style="font-size:12px;margin-bottom:8px;color:var(--muted)">Para guardar una embarcación:</div>
<div style="background:var(--s1);border:1px solid var(--b);border-radius:12px;padding:16px;max-width:320px;margin:0 auto;text-align:left">
<div style="font-size:12px;color:var(--text);margin-bottom:8px;display:flex;align-items:center;gap:8px">
<span style="background:rgba(0,229,160,.15);border:1px solid rgba(0,229,160,.3);border-radius:6px;padding:3px 10px;color:var(--green);font-size:11px;font-weight:700">☆ Guardar</span>
<span style="color:var(--muted)">Clic en el botón verde de cualquier tarjeta</span>
</div>
<div style="font-size:12px;color:var(--text);display:flex;align-items:center;gap:8px">
<span style="background:rgba(0,180,255,.1);border:1px solid rgba(0,180,255,.2);border-radius:6px;padding:3px 10px;color:var(--accent);font-size:11px;font-weight:700">📁 Carpetas</span>
<span style="color:var(--muted)">O guarda en carpetas por categoría</span>
</div>
</div>
<button class="btn-full" style="max-width:220px;margin:16px auto 0" onclick="switchTab('all',document.querySelector('.tab'))">Ver todas las embarcaciones</button>
</div>`;
} else {
$('content').innerHTML=`<div class="empty">
<div class="empty-icon">🚢</div>
<div style="font-size:15px;font-weight:600;margin-bottom:8px">No hay embarcaciones</div>
<div style="font-size:12px;margin-bottom:20px">Usa la búsqueda para encontrar resultados en todo internet</div>
<button class="btn-full" style="max-width:220px;margin:0 auto" onclick="openPanel('search')">⚡ Buscar ahora</button>
</div>`;
}
return;
}
if(state.view==='table') renderTable(v);
else renderGrid(v);
}
// Actions
async function toggleSave(id){
if(state.savedIds.has(id)){
await fetch('/api/saved/'+id,{method:'DELETE'});
state.savedIds.delete(id);toast('Eliminada de favoritos');
} else {
await fetch('/api/saved/'+id,{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});
state.savedIds.add(id);toast('★ Guardada');
}
renderContent();loadStats();
}
function doSearch(){
const q=$('mainSearch').value.trim();
if(!q) return;
$('aiQ').value=q;
openPanel('search');
}
let searchPoller = null;
async function runSearch(){
const q=$('aiQ').value.trim();
if(!q){toast('Escribe qué buscas');return;}
$('mainSearch').value=q;
$('srchBtn').textContent='⏳ Buscando…';
$('srchBtn').disabled=true;
$('srchResult').innerHTML=`
<div style="margin-top:14px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px">
<div class="spin" style="width:20px;height:20px;border-width:2px;flex-shrink:0"></div>
<span style="font-size:13px;font-weight:600" id="srchStatus">Iniciando búsqueda global…</span>
</div>
<div style="display:flex;gap:16px;margin-bottom:10px">
<div style="background:var(--s2);border-radius:8px;padding:8px 14px;text-align:center;flex:1">
<div style="font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em">Fuentes</div>
<div style="font-size:20px;font-weight:700;font-family:var(--mono);color:var(--accent)" id="srchSources">0</div>
</div>
<div style="background:var(--s2);border-radius:8px;padding:8px 14px;text-align:center;flex:1">
<div style="font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em">Encontradas</div>
<div style="font-size:20px;font-weight:700;font-family:var(--mono);color:var(--green)" id="srchFound">0</div>
</div>
</div>
<div style="background:var(--bg);border:1px solid var(--b);border-radius:8px;padding:10px;max-height:160px;overflow-y:auto;font-size:11px;font-family:var(--mono);color:var(--muted);line-height:1.8" id="srchLog">
Conectando con fuentes…
</div>
<button onclick="closePanel('search')" style="width:100%;margin-top:10px;background:var(--s2);border:1px solid var(--b);border-radius:10px;padding:10px;color:var(--text);cursor:pointer;font-size:13px;font-family:inherit">
Ver resultados en tiempo real →
</button>
</div>`;
try{
const filters={
type:$('aiType').value, status:$('aiStat').value,
max_price:$('aiPrice').value, min_loa:$('aiLoa').value,
max_loa:($('aiLoaMax')?$('aiLoaMax').value:''),
year_min:($('aiYearMin')?$('aiYearMin').value:''),
year_max:($('aiYearMax')?$('aiYearMax').value:''),
region:$('aiRegion').value, notes:$('aiNotes').value
};
await fetch('/api/search',{
method:'POST', headers:{'Content-Type':'application/json'},
body:JSON.stringify({query:q, filters})
});
// 1. Close panel immediately
closePanel('search');
// 2. Clear screen right away — no old results visible
state.vessels = [];
state.savedIds = new Set();
$('content').innerHTML = '<div class="empty" style="padding:40px 20px"><div style="font-size:36px;opacity:.4;margin-bottom:12px">🔍</div><div style="font-size:14px;font-weight:600;color:var(--muted)">Buscando &ldquo;'+q+'&rdquo;&hellip;</div><div style="font-size:12px;color:var(--muted);margin-top:6px">Los resultados aparecen aquí conforme se encuentran</div></div>';
loadStats();
// 3. Show live progress bar above grid
showSearchProgress(q);
// 4. Poll every 1.5s — append new vessels as they arrive
let lastCount = 0;
if(searchPoller) clearInterval(searchPoller);
searchPoller = setInterval(async()=>{
try{
const r = await fetch('/api/search/status');
const s = await r.json();
updateSearchProgress(s);
if(s.found > lastCount || (!s.running && s.found > 0)){
lastCount = s.found;
// Full reload — simple and reliable
await loadVessels();
loadStats();
}
if(!s.running){
clearInterval(searchPoller);
searchPoller = null;
$('srchBtn').textContent = '⚡ Buscar en Todo Internet';
$('srchBtn').disabled = false;
hideSearchProgress(s.found);
await loadVessels();
loadStats();
}
} catch(e){}
}, 1500);
}catch(e){
$('srchResult').innerHTML=`<div style="margin-top:12px;color:var(--red);font-size:12px">Error: ${e.message}</div>`;
$('srchBtn').textContent='⚡ Buscar en Todo Internet';
$('srchBtn').disabled=false;
}
}
async function runAnalyze(){
const txt=$('aTxt').value.trim();
if(!txt){toast('Pega un texto primero');return;}
$('aBtn').textContent='🤖 Analizando…';$('aBtn').disabled=true;
$('aResult').innerHTML=`<div style="text-align:center;padding:20px"><div class="spin" style="margin:0 auto 10px"></div><div style="font-size:12px;color:var(--muted)">Extrayendo datos con IA…</div></div>`;
try{
const r=await fetch('/api/analyze',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({text:txt,source:$('aSrc').value||'Manual'})});
const d=await r.json();
$('aResult').innerHTML=`<div style="background:var(--s2);border-radius:10px;padding:14px;margin-top:12px;font-size:12px;font-family:var(--mono);line-height:1.8;max-height:200px;overflow-y:auto">${JSON.stringify(d,null,2)}</div>
<button class="btn-full btn-sec" style="margin-top:8px" onclick="closePanel('analyze');loadVessels()">Ver en dashboard →</button>`;
loadStats();
}catch(e){
$('aResult').innerHTML=`<div style="margin-top:12px;color:var(--red);font-size:12px">Error: ${e.message}</div>`;
}
$('aBtn').textContent='🤖 Extraer con IA';$('aBtn').disabled=false;
}
async function addVessel(){
const v={
name:$('nName').value,vessel_type:$('nType').value,
loa_m:parseFloat($('nLoa').value)||null,beam_m:parseFloat($('nBeam').value)||null,
draft_m:parseFloat($('nDraft').value)||null,year_built:parseInt($('nYear').value)||null,
hull:$('nHull').value,propulsion:$('nProp').value,status:$('nStat').value,
price_usd:parseFloat($('nPrice').value)||null,
location:$('nLoc').value,source_name:$('nSrc2').value||'Manual',
source_url:$('nUrl').value,
images:$('nImg').value?[$('nImg').value]:[],
description:$('nDesc').value,score:50,
};
if(!v.name){toast('Escribe un nombre');return;}
const r=await fetch('/api/vessels',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(v)});
const d=await r.json();
if(d.ok){
await fetch('/api/saved/'+d.id,{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});
toast('✓ Guardada y marcada como favorita');
closePanel('add');loadVessels();loadStats();
}
}
function openSrc(id){
const v=state.vessels.find(x=>x.id===id);
if(!v||!v.source_url) return;
window.open(v.source_url,'_blank');
}
async function createAlert(){
const name=$('alName').value;
if(!name){toast('Escribe un nombre');return;}
const filters={type:$('alType').value,status:$('alStat').value,max_price:$('alPrice').value,min_loa:$('alLoa').value};
await fetch('/api/alerts',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name,filters})});
toast('🔔 Alerta creada');closePanel('alert');
}
async function loadAlerts(){
const r=await fetch('/api/alerts');const d=await r.json();
const alerts=d.alerts||[];
$('content').innerHTML=alerts.length
?`<div style="padding:16px 20px 80px">${alerts.map(a=>`<div class="card" style="padding:14px;margin-bottom:10px">
<div style="display:flex;justify-content:space-between;align-items:center">
<div><div style="font-weight:700">${a.name}</div><div style="font-size:11px;color:var(--muted);margin-top:3px">${a.filters}</div></div>
<button class="cbtn" onclick="delAlert(${a.id})">Eliminar</button>
</div></div>`).join('')}
<button class="btn-full btn-sec" style="margin-top:8px" onclick="openPanel('alert')">+ Nueva Alerta</button>
</div>`
:`<div class="empty"><div class="empty-icon">🔔</div><div>No hay alertas activas</div>
<button class="btn-full" style="max-width:200px;margin:16px auto 0" onclick="openPanel('alert')">Crear Alerta</button></div>`;
}
async function delAlert(id){
await fetch('/api/alerts/'+id,{method:'DELETE'});
toast('Alerta eliminada');loadAlerts();
}
async function loadHistory(){
const r=await fetch('/api/history');const d=await r.json();
const h=d.history||[];
$('content').innerHTML=h.length
?`<div style="padding:16px 20px 80px">${h.map(item=>`<div class="card" style="padding:12px 14px;margin-bottom:8px;cursor:pointer" onclick="$('aiQ').value='${item.query}';openPanel('search')">
<div style="display:flex;justify-content:space-between">
<span style="font-size:13px;font-weight:600">${item.query}</span>
<span style="font-size:10px;color:var(--muted)">${(item.searched_at||'').split('T')[0]}</span>
</div></div>`).join('')}</div>`
:`<div class="empty"><div class="empty-icon">🕐</div><div>No hay búsquedas recientes</div></div>`;
}
async function checkFbStatus(){
try{
const r=await fetch('/api/fb-status');
const d=await r.json();
const el=$('fbStatus');
const btn=$('fbSetupBtn');
if(!el) return;
if(d.active){
el.textContent='✅ Sesión activa — Marketplace habilitado';
el.style.color='var(--green)';
btn.textContent='Reconectar FB';
btn.style.background='rgba(0,200,100,.1)';
btn.style.borderColor='rgba(0,200,100,.3)';
btn.style.color='var(--green)';
} else {
el.textContent='⚪ No configurado — haz clic para activar';
el.style.color='var(--muted)';
btn.textContent='Configurar FB';
}
} catch(e){}
}
async function setupFacebook(){
const btn=$('fbSetupBtn');
const el=$('fbStatus');
btn.disabled=true;
btn.textContent='⏳ Abriendo navegador…';
el.textContent='Se abrirá una ventana — inicia sesión en Facebook y luego ve a Marketplace';
el.style.color='var(--amber)';
try{
const r=await fetch('/api/fb-setup',{method:'POST'});
const d=await r.json();
if(d.ok){
el.textContent='✅ '+d.msg;
el.style.color='var(--green)';
btn.textContent='Reconectar FB';
toast('✅ Facebook Marketplace activado');
} else {
el.textContent='❌ '+d.msg;
el.style.color='var(--red)';
btn.textContent='Reintentar';
}
} catch(e){
el.textContent='❌ Error de conexión';
el.style.color='var(--red)';
btn.textContent='Reintentar';
}
btn.disabled=false;
}
async function loadSources(){
checkFbStatus();
const r=await fetch('/api/sources');const d=await r.json();
const sources=d.sources||{};
const total=d.total||0;
const TC={auction:'var(--amber)',broker:'var(--accent)',classifieds:'var(--green)',
salvage:'var(--red)',news:'var(--purple)',magazine:'var(--purple)',
registry:'var(--muted)',commercial:'var(--muted)'};
let html = `<div style="font-size:12px;color:var(--muted);margin-bottom:12px">
<span style="color:var(--accent);font-weight:700">${total}</span> fuentes activas — la IA las consulta en cada búsqueda
</div>`;
for(const [cat, items] of Object.entries(sources)){
html += `<div class="src-cat">
<div class="src-cat-title">${cat} (${items.length})</div>
<div class="src-grid">
${items.map(s=>`<div class="src-item" style="${!s.active?'opacity:.4':''};${!s.builtin?'border-color:rgba(0,229,160,.25)':''}">
<div style="flex:1;min-width:0">
<div class="src-name" style="display:flex;align-items:center;gap:5px">
${!s.builtin?'<span style="color:var(--green);font-size:9px">★</span>':''}
${s.name}
</div>
<div class="src-type" style="color:${TC[s.type]||'var(--muted)'};">${s.type}</div>
</div>
<div style="display:flex;gap:4px;align-items:center;flex-shrink:0">
<a href="${s.url}" target="_blank" style="color:var(--accent);font-size:11px;text-decoration:none;padding:2px 6px">→</a>
${!s.builtin?`
<button onclick="toggleSource(${s.id},${s.active})" style="background:none;border:1px solid var(--b);border-radius:5px;padding:2px 7px;font-size:10px;cursor:pointer;color:${s.active?'var(--amber)':'var(--green)'};font-family:inherit">
${s.active?'⏸':'▶'}
</button>
<button onclick="deleteSource(${s.id})" style="background:none;border:1px solid var(--b);border-radius:5px;padding:2px 7px;font-size:10px;cursor:pointer;color:var(--red);font-family:inherit">✕</button>
`:''}
</div>
</div>`).join('')}
</div>
</div>`;
}
$('srcList').innerHTML = html;
}
async function addCustomSource(){
const name=$('newSrcName').value.trim();
const url=$('newSrcUrl').value.trim();
const type=$('newSrcType').value;
const cat=$('newSrcCat').value.trim()||'Custom';
if(!name||!url){toast('Nombre y URL son requeridos');return;}
if(!url.startsWith('http')){toast('La URL debe empezar con http:// o https://');return;}
const r=await fetch('/api/custom_sources',{method:'POST',
headers:{'Content-Type':'application/json'},credentials:'include',
body:JSON.stringify({name,search_url:url,source_type:type,category:cat})});
const d=await r.json();
if(d.ok){
$('newSrcName').value='';$('newSrcUrl').value='';$('newSrcCat').value='';
toast('✓ Fuente agregada — activa en próxima búsqueda');
loadSources();
} else {
toast('Error: '+d.error);
}
}
async function toggleSource(id,active){
await fetch('/api/custom_sources/'+id,{method:'PUT',
headers:{'Content-Type':'application/json'},credentials:'include',
body:JSON.stringify({active:active?0:1})});
toast(active?'Fuente pausada':'Fuente activada');
loadSources();
}
async function deleteSource(id){
if(!confirm('¿Eliminar esta fuente?'))return;
await fetch('/api/custom_sources/'+id,{method:'DELETE',credentials:'include'});
toast('Fuente eliminada');
loadSources();
}
// UI
function switchTab(t,el){
state.tab=t;
document.querySelectorAll('.tab').forEach(b=>b.classList.remove('on'));
el.classList.add('on');
$('fltbar').style.display=['all','auction','salvage'].includes(t)?'flex':'none';
if(t==='alerts') loadAlerts();
else if(t==='history') loadHistory();
else if(t==='collections') loadCollectionsTab();
else loadVessels();
}
function toggleView(){
state.view=state.view==='grid'?'table':'grid';
$('viewBtn').textContent=state.view==='grid'?'☰ Tabla':'⊞ Grid';
renderContent();
}
function openPanel(n){
if(n==='search') $('pSearch').classList.add('show');
else if(n==='analyze') $('pAnalyze').classList.add('show');
else if(n==='add') $('pAdd').classList.add('show');
else if(n==='sources'){loadSources();$('pSources').classList.add('show');}
else if(n==='alert') $('pAlert').classList.add('show');
}
function closePanel(n){
['pSearch','pAnalyze','pAdd','pSources','pAlert'].forEach(id=>$('content'),$( 'p'+n.charAt(0).toUpperCase()+n.slice(1))?.classList.remove('show'));
$('pSearch').classList.remove('show');$('pAnalyze').classList.remove('show');
$('pAdd').classList.remove('show');$('pSources').classList.remove('show');
$('pAlert').classList.remove('show');
}
function toast(msg){
const el=$('toast');el.textContent=msg;el.classList.add('show');
setTimeout(()=>el.classList.remove('show'),2200);
}
// ── Live search progress bar ──────────────────────────────────────────────
function showSearchProgress(query){
// Remove existing if any
const existing = $('liveProgress');
if(existing) existing.remove();
const bar = document.createElement('div');
bar.id = 'liveProgress';
bar.style.cssText = 'background:var(--s1);border-bottom:1px solid rgba(0,180,255,.25);padding:10px 20px;display:flex;align-items:center;gap:14px;flex-wrap:wrap;position:sticky;top:0;z-index:150;backdrop-filter:blur(8px)';
bar.innerHTML = `
<div style="width:16px;height:16px;border:2px solid rgba(0,180,255,.3);border-top-color:var(--accent);border-radius:50%;animation:spin 1s linear infinite;flex-shrink:0" id="lpSpinner"></div>
<span style="font-size:12px;font-weight:700;color:var(--accent)" id="lpQuery">"${query}"</span>
<span style="font-size:12px;color:var(--muted)" id="lpStatus">Consultando fuentes…</span>
<div style="margin-left:auto;display:flex;gap:12px;align-items:center">
<div style="text-align:center">
<div style="font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em">Fuentes</div>
<div style="font-size:16px;font-weight:700;font-family:var(--mono);color:var(--accent)" id="lpSources">0</div>
</div>
<div style="text-align:center">
<div style="font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.06em">Encontradas</div>
<div style="font-size:16px;font-weight:700;font-family:var(--mono);color:var(--green)" id="lpFound">0</div>
</div>
<div style="width:100px;height:4px;background:var(--b2);border-radius:2px;overflow:hidden">
<div id="lpBar" style="height:100%;background:var(--accent);border-radius:2px;width:0%;transition:width .5s"></div>
</div>
<button onclick="cancelSearch()" id="lpCancel" style="background:rgba(255,82,82,.12);border:1px solid rgba(255,82,82,.3);border-radius:8px;padding:5px 12px;color:var(--red);font-size:11px;font-weight:700;cursor:pointer;font-family:inherit;white-space:nowrap">⏹ Parar</button>
</div>`;
// Insert after filters bar
const fltbar = $('fltbar');
fltbar.insertAdjacentElement('afterend', bar);
}
function updateSearchProgress(s){
const lpStatus = $('lpStatus');
const lpSources = $('lpSources');
const lpFound = $('lpFound');
const lpBar = $('lpBar');
if(!lpStatus) return;
const pct = s.total_sources > 0 ? Math.round((s.sources_done/s.total_sources)*100) : 0;
lpStatus.textContent = s.running ? `Consultando fuentes… (${s.sources_done}/${s.total_sources})` : '✓ Completado';
lpSources.textContent = `${s.sources_done}/${s.total_sources}`;
lpFound.textContent = s.found || 0;
if(lpBar) lpBar.style.width = pct + '%';
}
function hideSearchProgress(found){
const bar = $('liveProgress');
if(!bar) return;
// Show completion briefly then fade out
const lpStatus = $('lpStatus');
if(lpStatus) lpStatus.textContent = `✓ Búsqueda completa — ${found} embarcaciones encontradas`;
const lpBar = $('lpBar');
if(lpBar) lpBar.style.width = '100%';
setTimeout(()=>{
if(bar){ bar.style.transition='opacity .5s'; bar.style.opacity='0'; }
setTimeout(()=>{ const b=$('liveProgress'); if(b)b.remove(); }, 500);
}, 3000);
}
// ── Cancel search ─────────────────────────────────────────────────────────
async function cancelSearch(){
if(searchPoller){ clearInterval(searchPoller); searchPoller = null; }
try{ await fetch('/api/search/cancel', {method:'POST'}); }catch(e){}
const btn = $('lpCancel');
const spinner = $('lpSpinner');
const lpStatus = $('lpStatus');
if(btn){ btn.style.display='none'; }
if(spinner){ spinner.style.borderTopColor='var(--amber)'; spinner.style.animation='none'; }
if(lpStatus){ lpStatus.textContent='⏹ Búsqueda cancelada'; lpStatus.style.color='var(--amber)'; }
$('srchBtn').textContent='⚡ Buscar en Todo Internet';
$('srchBtn').disabled=false;
loadVessels(); loadStats();
setTimeout(()=>{ const b=$('liveProgress'); if(b){ b.style.transition='opacity .5s'; b.style.opacity='0'; setTimeout(()=>{ const b2=$('liveProgress'); if(b2)b2.remove(); },500); }}, 2500);
}
// ── Selection ─────────────────────────────────────────────────────────────
function toggleSelect(id,e){
if(e)e.stopPropagation();
if(state.selected.has(id))state.selected.delete(id);
else state.selected.add(id);
updateSelBar();
const chk=$('chk-'+id);
if(chk){chk.style.background=state.selected.has(id)?'var(--accent)':'rgba(0,0,0,.4)';chk.textContent=state.selected.has(id)?'✓':'';}
}
function clearSelection(){state.selected.clear();updateSelBar();renderContent();}
function updateSelBar(){
const bar=$('selBar');if(!bar)return;
if(state.selected.size>0){bar.style.display='flex';$('selCount').textContent=state.selected.size+' seleccionada'+(state.selected.size>1?'s':'');}
else bar.style.display='none';
}
// ── Collections ────────────────────────────────────────────────────────────
async function loadCollections(){
const r=await fetch('/api/collections');const d=await r.json();
state.collections=d.collections||[];
}
async function loadCollectionsTab(){
await loadCollections();
const cols=state.collections;
$('fltbar').style.display='none';
$('content').innerHTML=`<div style="padding:16px 20px 100px">
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px">
${cols.map(c=>`<div class="card" style="cursor:pointer" onclick="openCollection(${c.id},'${c.name.replace(/'/g,"\\'")}')">
<div style="font-size:32px;margin-bottom:8px">${c.icon||'📁'}</div>
<div style="font-weight:700;font-size:14px;margin-bottom:4px">${c.name}</div>
${c.description?`<div style="font-size:11px;color:var(--muted);margin-bottom:8px">${c.description}</div>`:''}
<div style="font-size:13px;color:var(--accent);font-weight:600;margin-bottom:10px">${c.vessel_count||0} embarcaciones</div>
<button onclick="event.stopPropagation();deleteCollection(${c.id})" style="background:none;border:1px solid var(--b);border-radius:6px;padding:4px 10px;color:var(--muted);font-size:11px;cursor:pointer;font-family:inherit">Eliminar</button>
</div>`).join('')}
<div class="card" onclick="openNewColModal()" style="cursor:pointer;border-style:dashed;min-height:120px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px">
<div style="font-size:32px;opacity:.35">+</div>
<div style="font-size:12px;color:var(--muted)">Nueva carpeta</div>
</div>
</div>
</div>`;
}
async function openCollection(id,name){
const r=await fetch('/api/collections/'+id+'/vessels');
const d=await r.json();
state.vessels=d.vessels||[];
state.savedIds=new Set(state.vessels.map(v=>v.id));
$('content').innerHTML='';
const hdr=document.createElement('div');
hdr.style.cssText='padding:10px 20px;display:flex;align-items:center;gap:12px;border-bottom:1px solid var(--b);margin-bottom:4px';
hdr.innerHTML=`<button onclick="loadCollectionsTab()" style="background:none;border:none;color:var(--muted);cursor:pointer;font-size:13px;font-family:inherit">← Carpetas</button>
<span style="font-weight:700;font-size:15px">${name}</span>
<span style="font-size:12px;color:var(--muted)">${state.vessels.length} embarcaciones</span>`;
$('content').appendChild(hdr);
if(state.vessels.length){
const grid=document.createElement('div');grid.className='grid';
grid.innerHTML=state.vessels.map(card).join('');
$('content').appendChild(grid);
} else {
const empty=document.createElement('div');empty.className='empty';
empty.innerHTML='<div class="empty-icon">📭</div><div>Carpeta vacía</div>';
$('content').appendChild(empty);
}
}
async function saveSelectedToCollection(){
if(state.selected.size===0){toast('Selecciona embarcaciones primero');return;}
await loadCollections();
showColPickerModal([...state.selected]);
}
function showColPickerModal(ids){
const cols=state.collections;
const modal=document.createElement('div');
modal.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px';
modal.innerHTML=`<div style="background:var(--s1);border:1px solid var(--b2);border-radius:14px;padding:22px;max-width:440px;width:100%;font-family:var(--head)">
<div style="font-size:15px;font-weight:800;margin-bottom:16px">📁 Guardar ${ids.length} embarcación${ids.length>1?'es':''}</div>
${cols.length?`<div style="margin-bottom:14px">
<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px">Carpetas existentes</div>
<div style="display:flex;flex-direction:column;gap:6px;max-height:180px;overflow-y:auto" id="colList">
${cols.map(c=>`<button onclick="doAddToCol(${c.id})" style="background:var(--s2);border:1px solid var(--b);border-radius:9px;padding:10px 14px;text-align:left;cursor:pointer;display:flex;align-items:center;gap:10px;font-family:inherit">
<span style="font-size:20px">${c.icon||'📁'}</span>
<div><div style="font-size:13px;font-weight:600;color:var(--text)">${c.name}</div><div style="font-size:11px;color:var(--muted)">${c.vessel_count||0} embarcaciones</div></div>
</button>`).join('')}
</div>
</div>`:''}
<div style="border-top:1px solid var(--b);padding-top:14px">
<div style="font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;margin-bottom:8px">Nueva carpeta</div>
<div style="display:flex;gap:8px;margin-bottom:8px">
<input id="mIcon" value="📁" style="width:46px;background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:7px;color:var(--text);font-size:18px;text-align:center">
<input id="mName" placeholder="Ej: Tugs Diesel 15m+" style="flex:1;background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:7px 12px;color:var(--text);font-size:13px">
</div>
<input id="mDesc" placeholder="Descripción opcional" style="width:100%;background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:7px 12px;color:var(--text);font-size:12px;margin-bottom:8px">
<button onclick="doCreateAndAdd()" style="width:100%;background:var(--accent);color:#000;border:none;border-radius:9px;padding:11px;font-size:13px;font-weight:800;cursor:pointer">Crear y guardar</button>
</div>
<button onclick="this.closest('[style*=fixed]').remove()" style="width:100%;margin-top:8px;background:none;border:1px solid var(--b);border-radius:9px;padding:10px;color:var(--muted);cursor:pointer;font-size:12px;font-family:inherit">Cancelar</button>
</div>`;
window.doAddToCol = async function(cid){
modal.remove();
await fetch('/api/collections/'+cid+'/vessels',{method:'POST',
headers:{'Content-Type':'application/json'},body:JSON.stringify({vessel_ids:ids})});
toast('✓ Guardadas en carpeta');
clearSelection();
loadStats();
};
window.doCreateAndAdd = async function(){
const name=$('mName').value.trim();
if(!name){toast('Escribe un nombre');return;}
const r=await fetch('/api/collections',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({name,icon:$('mIcon').value||'📁',description:$('mDesc').value})});
const d=await r.json();
if(d.id) window.doAddToCol(d.id);
};
modal.onclick=e=>{if(e.target===modal)modal.remove();};
document.body.appendChild(modal);
}
async function deleteCollection(id){
if(!confirm('¿Eliminar esta carpeta?'))return;
await fetch('/api/collections/'+id,{method:'DELETE'});
toast('Carpeta eliminada');loadCollectionsTab();
}
function openNewColModal(){
const modal=document.createElement('div');
modal.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px';
modal.innerHTML=`<div style="background:var(--s1);border:1px solid var(--b2);border-radius:14px;padding:22px;max-width:380px;width:100%;font-family:var(--head)">
<div style="font-size:15px;font-weight:800;margin-bottom:16px">📁 Nueva Carpeta</div>
<div style="display:flex;gap:8px;margin-bottom:10px">
<input id="nc3Icon" value="📁" style="width:46px;background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:7px;color:var(--text);font-size:18px;text-align:center">
<input id="nc3Name" placeholder="Ej: Pesqueros Acero Noruega" style="flex:1;background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:7px 12px;color:var(--text);font-size:13px">
</div>
<input id="nc3Desc" placeholder="Descripción opcional" style="width:100%;background:var(--s2);border:1px solid var(--b);border-radius:8px;padding:7px 12px;color:var(--text);font-size:12px;margin-bottom:10px">
<button onclick="doCreateCol()" style="width:100%;background:var(--accent);color:#000;border:none;border-radius:9px;padding:11px;font-size:13px;font-weight:800;cursor:pointer">Crear Carpeta</button>
<button onclick="this.closest('[style*=fixed]').remove()" style="width:100%;margin-top:8px;background:none;border:1px solid var(--b);border-radius:9px;padding:10px;color:var(--muted);cursor:pointer;font-size:12px;font-family:inherit">Cancelar</button>
</div>`;
window.doCreateCol=async function(){
const name=$('nc3Name').value.trim();
if(!name){toast('Escribe un nombre');return;}
await fetch('/api/collections',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({name,icon:$('nc3Icon').value||'📁',description:$('nc3Desc').value})});
modal.remove();toast('✓ Carpeta creada');loadCollectionsTab();
};
modal.onclick=e=>{if(e.target===modal)modal.remove();};
document.body.appendChild(modal);
}
// ── Init ───────────────────────────────────────────────────────────────────
checkStatus();
loadVessels();
setInterval(checkStatus,30000);
</script>
</body>
</html>