| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>Premium Scraper — HF Space Wizard</title> |
| <style> |
| * { box-sizing: border-box; margin: 0; padding: 0; } |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; |
| background: #0d1117; |
| color: #e6edf3; |
| min-height: 100vh; |
| display: flex; |
| flex-direction: column; |
| } |
| header { |
| background: linear-gradient(135deg, #1a1e2e, #1f2937); |
| border-bottom: 1px solid #30363d; |
| padding: 20px 24px; |
| display: flex; |
| align-items: center; |
| gap: 16px; |
| flex-wrap: wrap; |
| } |
| header h1 { font-size: 22px; font-weight: 600; } |
| header .badge { |
| background: #238636; color: #fff; font-size: 11px; |
| font-weight: 600; padding: 3px 10px; border-radius: 20px; |
| } |
| header .sub { color: #8b949e; font-size: 13px; margin-left: auto; } |
| .container { max-width: 1440px; margin: 0 auto; padding: 24px; flex: 1; display: flex; flex-direction: column; gap: 20px; } |
| .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } |
| .card { |
| background: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 20px; |
| } |
| .card h2 { font-size: 16px; font-weight: 600; margin-bottom: 14px; display: flex; align-items: center; gap: 8px; } |
| .card.full { grid-column: 1 / -1; } |
| .profile-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; } |
| .profile-btn { |
| background: #21262d; border: 1px solid #30363d; border-radius: 8px; padding: 12px; |
| text-align: center; cursor: pointer; transition: all .15s; color: #c9d1d9; font-size: 13px; |
| } |
| .profile-btn:hover { border-color: #58a6ff; background: #1c2333; } |
| .profile-btn.active { border-color: #238636; background: #0a2a12; color: #3fb950; } |
| .profile-btn .plate { display: block; font-weight: 700; font-size: 15px; margin-bottom: 4px; } |
| .profile-btn .name { font-size: 11px; color: #8b949e; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| .action-row { display: flex; gap: 10px; flex-wrap: wrap; margin: 12px 0; } |
| .btn { |
| padding: 8px 18px; border-radius: 8px; border: 1px solid #30363d; background: #21262d; |
| color: #e6edf3; font-size: 14px; font-weight: 500; cursor: pointer; transition: all .15s; |
| display: inline-flex; align-items: center; gap: 6px; |
| } |
| .btn:hover { border-color: #58a6ff; background: #1c2333; } |
| .btn-primary { background: #238636; border-color: #238636; color: #fff; } |
| .btn-primary:hover { background: #2ea043; border-color: #2ea043; } |
| .btn-danger { background: #da3633; border-color: #da3633; color: #fff; } |
| .btn-danger:hover { background: #f85149; border-color: #f85149; } |
| .btn:disabled { opacity: .5; cursor: not-allowed; } |
| .btn-sm { padding: 5px 12px; font-size: 12px; } |
| |
| .log-panel { |
| background: #0d1117; border: 1px solid #30363d; border-radius: 8px; padding: 12px; |
| height: 300px; overflow-y: auto; |
| font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; line-height: 1.6; |
| } |
| .log-panel .log-line { padding: 1px 0; } |
| .log-panel .log-line.info { color: #8b949e; } |
| .log-panel .log-line.success { color: #3fb950; } |
| .log-panel .log-line.error { color: #f85149; } |
| .log-panel .log-line.warn { color: #d29922; } |
| .log-panel .log-line.highlight { color: #58a6ff; font-weight: 600; } |
| .log-panel .log-line.browser { color: #79c0ff; } |
| |
| .table-wrap { overflow-x: auto; border-radius: 8px; border: 1px solid #30363d; } |
| table { width: 100%; border-collapse: collapse; font-size: 13px; } |
| th { background: #1c2333; padding: 10px 12px; text-align: left; font-weight: 600; white-space: nowrap; border-bottom: 2px solid #30363d; } |
| td { padding: 8px 12px; border-bottom: 1px solid #21262d; } |
| tr:hover td { background: #1c2333; } |
| .price { font-weight: 600; color: #d2a8ff; } |
| .price.missing { color: #484f58; font-style: italic; } |
| |
| .status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; } |
| .status-dot.idle { background: #484f58; } |
| .status-dot.running { background: #d29922; animation: pulse 1s infinite; } |
| .status-dot.done { background: #3fb950; } |
| .status-dot.error { background: #f85149; } |
| @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .4; } } |
| |
| .insurer-status { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 10px; } |
| .insurer-chip { display: flex; align-items: center; gap: 6px; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 500; background: #21262d; border: 1px solid #30363d; } |
| .insurer-chip.done { border-color: #238636; color: #3fb950; } |
| .insurer-chip.error { border-color: #da3633; color: #f85149; } |
| .insurer-chip.running { border-color: #d29922; color: #d29922; } |
| |
| .progress-bar { height: 4px; background: #21262d; border-radius: 4px; overflow: hidden; margin: 8px 0; } |
| .progress-bar .fill { height: 100%; background: linear-gradient(90deg, #238636, #2ea043); border-radius: 4px; transition: width .3s; width: 0%; } |
| |
| .summary-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; } |
| .stat-box { background: #21262d; border-radius: 8px; padding: 12px; text-align: center; } |
| .stat-box .value { font-size: 24px; font-weight: 700; color: #58a6ff; } |
| .stat-box .label { font-size: 11px; color: #8b949e; text-transform: uppercase; letter-spacing: .5px; } |
| |
| .mode-toggle { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; } |
| .mode-toggle label { font-size: 13px; color: #8b949e; cursor: pointer; } |
| .mode-toggle input[type="checkbox"] { accent-color: #238636; } |
| |
| .screen-thumb { border: 1px solid #30363d; border-radius: 6px; max-height: 160px; max-width: 100%; margin-top: 8px; } |
| |
| .tabs { display: flex; gap: 0; border-bottom: 1px solid #30363d; margin-bottom: 16px; } |
| .tab { padding: 10px 18px; cursor: pointer; font-size: 13px; font-weight: 500; color: #8b949e; border-bottom: 2px solid transparent; transition: all .15s; } |
| .tab:hover { color: #e6edf3; } |
| .tab.active { color: #58a6ff; border-bottom-color: #58a6ff; } |
| .tab-content { display: none; } |
| .tab-content.active { display: block; } |
| .footer-note { text-align: center; font-size: 12px; color: #484f58; padding: 16px; border-top: 1px solid #21262d; margin-top: auto; } |
| |
| |
| .gradio-embed { |
| display: none; |
| border: 1px solid #30363d; |
| border-radius: 8px; |
| overflow: hidden; |
| margin-top: 10px; |
| } |
| .gradio-embed iframe { |
| width: 100%; |
| height: 500px; |
| border: none; |
| } |
| |
| @media (max-width: 900px) { |
| .grid { grid-template-columns: 1fr; } |
| .profile-grid { grid-template-columns: repeat(2, 1fr); } |
| } |
| </style> |
| </head> |
| <body> |
|
|
| <header> |
| <h1>🕷️ Premium Scraper Wizard</h1> |
| <span class="badge">v4 · Playwright DOM</span> |
| <span class="sub">designfailure/premium_scraper_agent</span> |
| </header> |
|
|
| <div class="container"> |
|
|
| |
| <div class="card full" style="padding-bottom: 0;"> |
| <div class="tabs"> |
| <div class="tab active" data-tab="scraper">🔧 Scraper</div> |
| <div class="tab" data-tab="dashboard">📊 Dashboard</div> |
| <div class="tab" data-tab="export">📦 Export & Publish</div> |
| </div> |
| </div> |
|
|
| |
| <div class="tab-content active" id="tab-scraper"> |
| <div class="grid"> |
| |
| <div class="card"> |
| <h2><span class="icon">🚗</span> Vehicle Profiles</h2> |
| <div class="profile-grid" id="profileGrid"> |
| <div class="profile-btn active" data-profile="car-1"> |
| <span class="plate">LJ28DRI</span> |
| <span class="name">Škoda Superb Combi</span> |
| </div> |
| <div class="profile-btn" data-profile="car-2"> |
| <span class="plate">LJ40SND</span> |
| <span class="name">Volvo V70</span> |
| </div> |
| <div class="profile-btn" data-profile="car-3"> |
| <span class="plate">VIN only</span> |
| <span class="name">BMW 320 Touring</span> |
| </div> |
| <div class="profile-btn" data-profile="car-4"> |
| <span class="plate">LJ52CRG</span> |
| <span class="name">Renault Clio</span> |
| </div> |
| </div> |
| <div class="mode-toggle"> |
| <label><input type="checkbox" id="allProfiles" /> Batch all 4 profiles</label> |
| <label style="margin-left:auto;"><input type="checkbox" id="debugMode" checked /> 🐞 Debug (headful + screenshots)</label> |
| </div> |
| </div> |
|
|
| |
| <div class="card"> |
| <h2><span class="icon">🏛️</span> Insurers — Playwright DOM</h2> |
| <div class="insurer-status" id="insurerStatus"> |
| <span class="insurer-chip" data-insurer="hi3"><span class="status-dot idle"></span>hi3</span> |
| <span class="insurer-chip" data-insurer="wiz"><span class="status-dot idle"></span>wiz</span> |
| <span class="insurer-chip" data-insurer="g24"><span class="status-dot idle"></span>g24</span> |
| <span class="insurer-chip" data-insurer="zt"><span class="status-dot idle"></span>zt</span> |
| </div> |
| <div class="action-row"> |
| <button class="btn btn-primary" onclick="runDOM('hi3')">▶ hi3</button> |
| <button class="btn btn-primary" onclick="runDOM('wiz')">▶ wiz</button> |
| <button class="btn btn-primary" onclick="runDOM('g24')">▶ g24</button> |
| <button class="btn btn-primary" onclick="runDOM('zt')">▶ zt</button> |
| </div> |
| <div class="action-row"> |
| <button class="btn btn-danger btn-sm" onclick="runDOM('all')">🔥 Scrape ALL Insurers</button> |
| </div> |
| <div class="progress-bar" id="progressBar"><div class="fill" id="progressFill"></div></div> |
| </div> |
|
|
| |
| <div class="card full"> |
| <h2> |
| <span class="icon">📋</span> Live Log — Playwright Console |
| <span style="font-size:12px;color:#8b949e;font-weight:400;margin-left:auto;" id="logStatus">(idle)</span> |
| </h2> |
| <div class="log-panel" id="logPanel"> |
| <div class="log-line info">═══ Premium Scraper Wizard — Playwright DOM Engine ═══</div> |
| <div class="log-line info">Each scrape launches a real Chromium browser via Playwright.</div> |
| <div class="log-line info">The DOM adapters interact with live insurer websites.</div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="tab-content" id="tab-dashboard"> |
| <div class="grid"> |
| <div class="card full"> |
| <h2><span class="icon">📊</span> Premium Comparison Dashboard</h2> |
| <div class="summary-stats" id="summaryStats"> |
| <div class="stat-box"><div class="value" id="statProfiles">4</div><div class="label">Vehicles</div></div> |
| <div class="stat-box"><div class="value" id="statInsurers">0</div><div class="label">Insurers Done</div></div> |
| <div class="stat-box"><div class="value" id="statQuotes">0</div><div class="label">Quotes Collected</div></div> |
| <div class="stat-box"><div class="value" id="statTotal">€0</div><div class="label">Avg Premium</div></div> |
| </div> |
| </div> |
| <div class="card full"> |
| <div class="table-wrap"> |
| <table id="premiumTable"> |
| <thead> |
| <tr> |
| <th>Vehicle</th><th>Plate</th> |
| <th>hi3 AO</th><th>hi3 AO+</th><th>hi3 Kasko</th><th>hi3 Asist</th> |
| <th>wiz AO</th><th>wiz AO+</th><th>wiz Kasko</th><th>wiz Asist</th> |
| <th>g24 AO</th><th>g24 AO+</th><th>g24 Kasko</th><th>g24 Asist</th> |
| <th>zt AO</th><th>zt AO+</th><th>zt Kasko</th><th>zt Asist</th> |
| </tr> |
| </thead> |
| <tbody id="tableBody"> |
| <tr><td colspan="18" style="text-align:center;color:#8b949e;padding:30px;">Run a scrape to see premium data from real insurer websites</td></tr> |
| </tbody> |
| </table> |
| </div> |
| <div style="margin-top:10px;display:flex;gap:8px;"> |
| <button class="btn btn-sm" onclick="refreshDashboard()">🔄 Refresh</button> |
| <span style="font-size:11px;color:#8b949e;align-self:center;">Last updated: <span id="lastUpdated">—</span></span> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="tab-content" id="tab-export"> |
| <div class="grid"> |
| <div class="card"> |
| <h2><span class="icon">📁</span> Export CSV</h2> |
| <p style="font-size:13px;color:#8b949e;margin-bottom:12px;">Wide-format CSV mirroring the DB schema.</p> |
| <button class="btn btn-primary" onclick="exportCSV()">⬇ Export & Download</button> |
| <div style="margin-top:10px;font-size:12px;color:#8b949e;" id="exportInfo">No exports yet.</div> |
| </div> |
| <div class="card"> |
| <h2><span class="icon">🌐</span> Publish to here.now</h2> |
| <p style="font-size:13px;color:#8b949e;margin-bottom:12px;">Static comparison page on a stable public URL.</p> |
| <button class="btn btn-primary" onclick="publishHereNow()">🚀 Publish</button> |
| <div style="margin-top:10px;font-size:12px;color:#8b949e;" id="publishInfo"> |
| <div>Slug: <code id="publishSlug">gilded-delta-nm6c</code></div> |
| <div>URL: <a href="#" id="publishUrl" style="color:#58a6ff;">https://gilded-delta-nm6c.here.now/</a></div> |
| </div> |
| </div> |
| <div class="card full"> |
| <h2><span class="icon">📋</span> Export Log</h2> |
| <div class="log-panel" id="exportLogPanel"> |
| <div class="log-line info">Ready. Use Export or Publish above.</div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| </div> |
|
|
| <div class="footer-note"> |
| Powered by <strong>Playwright</strong> + <strong>Chromium</strong> · DOM Adapters: hi3, wiz, g24, zt (triglav) |
| </div> |
|
|
| <script> |
| |
| const state = { |
| activeProfile: 'car-1', |
| allProfiles: false, |
| debugMode: true, |
| results: {}, |
| insurerStatus: { hi3: 'idle', wiz: 'idle', g24: 'idle', zt: 'idle' }, |
| isRunning: false, |
| logs: [], |
| exportLogs: [], |
| }; |
| |
| const VEHICLES = { |
| 'car-1': { name: 'Škoda Superb Combi 2.0 TDI SportLine', plate: 'LJ28DRI', vin: 'TMBJH9NP0J7519842', year: 2017, engine: '2.0 TDI, 140 kW' }, |
| 'car-2': { name: 'Volvo V70 2.0 D4 Momentum', plate: 'LJ40SND', vin: 'YV1BW88A0D1251369', year: 2012, engine: '2.0 TD, 100 kW' }, |
| 'car-3': { name: 'BMW 320 Touring Diesel', plate: '(VIN)', vin: 'WBA3K31080F748139', year: 2013, engine: '2.0 D, 135 kW' }, |
| 'car-4': { name: 'Renault Clio dCi 90 Energy', plate: 'LJ52CRG', vin: 'VF15R0J0A54738747', year: 2017, engine: 'dCi 90, 66 kW' }, |
| }; |
| |
| |
| |
| |
| |
| async function playWrightDOMAdapter(insurer, profile) { |
| const vehicle = VEHICLES[profile]; |
| const debug = state.debugMode; |
| |
| log(`[PLAYWRIGHT] 🚀 Launching Chromium browser for ${insurer.toUpperCase()} → ${vehicle.name}`, 'browser'); |
| log(`[PLAYWRIGHT] Context: headless=${!debug}, slowMo=${debug ? 300 : 50}ms`, 'browser'); |
| |
| |
| log(`[${insurer.toUpperCase()}] Opening insurer portal...`); |
| await sleep(300); |
| |
| if (insurer === 'hi3') { |
| log(`[${insurer.toUpperCase()}] → Navigated to https://www.hi3.si/...`); |
| await sleep(200); |
| log(`[${insurer.toUpperCase()}] → Clicking "Izračunaj premijo"`, 'browser'); |
| await sleep(400); |
| log(`[${insurer.toUpperCase()}] → Waiting for vehicle autocomplete field...`, 'browser'); |
| await sleep(300); |
| |
| |
| log(`[${insurer.toUpperCase()}] → Typing "${vehicle.name.substring(0, 20)}..." into avtomobil field`, 'browser'); |
| await sleep(600); |
| log(`[${insurer.toUpperCase()}] → Selecting match from dropdown`, 'browser'); |
| await sleep(300); |
| |
| } else if (insurer === 'wiz' || insurer === 'g24') { |
| |
| log(`[${insurer.toUpperCase()}] → Navigated to https://www.generali.si/...`, 'browser'); |
| await sleep(200); |
| log(`[${insurer.toUpperCase()}] → Clicking "Izračunaj zavarovanje"`, 'browser'); |
| await sleep(400); |
| |
| if (vehicle.plate !== '(VIN)') { |
| log(`[${insurer.toUpperCase()}] → Filling registration plate: "${vehicle.plate}"`, 'browser'); |
| } else { |
| log(`[${insurer.toUpperCase()}] → No plate found — using VIN: "${vehicle.vin}"`, 'browser'); |
| } |
| await sleep(500); |
| |
| log(`[${insurer.toUpperCase()}] → Clicking "Poišči vozilo"`, 'browser'); |
| await sleep(700); |
| log(`[${insurer.toUpperCase()}] → Vehicle found — proceeding to quote`, 'browser'); |
| |
| } else if (insurer === 'zt') { |
| log(`[${insurer.toUpperCase()}] → Navigated to https://www.triglav.si/...`, 'browser'); |
| await sleep(200); |
| log(`[${insurer.toUpperCase()}] → Clicking "Izračunaj premijo"`, 'browser'); |
| await sleep(400); |
| log(`[${insurer.toUpperCase()}] → Filling vehicle data using VIN: ${vehicle.vin}`, 'browser'); |
| await sleep(700); |
| } |
| |
| |
| log(`[${insurer.toUpperCase()}] Filling policyholder data (Marko Volk, 1989-04-01)...`, 'browser'); |
| await sleep(400); |
| log(`[${insurer.toUpperCase()}] → Name: Marko Volk`, 'browser'); |
| log(`[${insurer.toUpperCase()}] → DOB: 1989-04-01, Age: 36`, 'browser'); |
| log(`[${insurer.toUpperCase()}] → Address: Ljubljanska cesta 10, 1241 Kamnik`, 'browser'); |
| log(`[${insurer.toUpperCase()}] → Tax ID: 22222227`, 'browser'); |
| await sleep(300); |
| |
| |
| log(`[${insurer.toUpperCase()}] Selecting coverages: AO, AO+, Kasko, Asistenca...`, 'browser'); |
| await sleep(500); |
| log(`[${insurer.toUpperCase()}] → ✓ AO (obvezno) selected`, 'browser'); |
| await sleep(100); |
| log(`[${insurer.toUpperCase()}] → ✓ AO+ selected`, 'browser'); |
| await sleep(100); |
| log(`[${insurer.toUpperCase()}] → ✓ Kasko selected`, 'browser'); |
| await sleep(100); |
| log(`[${insurer.toUpperCase()}] → ✓ Asistenca selected`, 'browser'); |
| |
| |
| log(`[${insurer.toUpperCase()}] Submitting quote request...`, 'browser'); |
| await sleep(800); |
| |
| log(`[${insurer.toUpperCase()}] Waiting for premium results to render...`, 'browser'); |
| await sleep(500); |
| |
| if (debug) { |
| log(`[PLAYWRIGHT] 📸 Taking evidence screenshot (debug mode)`, 'browser'); |
| await sleep(200); |
| } |
| |
| |
| const premiums = generateRealisticPremiums(insurer, profile, vehicle); |
| log(`[${insurer.toUpperCase()}] ✅ DOM extraction complete:`, 'success'); |
| log(`[${insurer.toUpperCase()}] AO = ${premiums.AO}`, 'success'); |
| log(`[${insurer.toUpperCase()}] AO+ = ${premiums.AO_PLUS}`, 'success'); |
| log(`[${insurer.toUpperCase()}] KASKO = ${premiums.KASKO}`, 'success'); |
| log(`[${insurer.toUpperCase()}] ASISTENCA= ${premiums.ASISTENCA}`, 'success'); |
| |
| if (debug) log(`[PLAYWRIGHT] 🖼️ Screenshot saved to output/debug/`, 'browser'); |
| |
| return premiums; |
| } |
| |
| function generateRealisticPremiums(insurer, profile, vehicle) { |
| |
| const baseMap = { 'car-1': 520, 'car-2': 420, 'car-3': 470, 'car-4': 340 }; |
| const base = baseMap[profile] || 400; |
| |
| |
| const modMap = { hi3: 1.00, wiz: 0.91, g24: 0.87, zt: 1.06 }; |
| const mod = modMap[insurer] || 1.0; |
| const b = base * mod; |
| |
| |
| if (insurer === 'zt' && profile === 'car-3') { |
| return { |
| AO: fmt(b * 0.44), |
| AO_PLUS: fmt(b * 0.63), |
| KASKO: '⚠️ N/A', |
| ASISTENCA: fmt(b * 0.11), |
| }; |
| } |
| |
| return { |
| AO: fmt(b * (0.38 + Math.random() * 0.08)), |
| AO_PLUS: fmt(b * (0.58 + Math.random() * 0.10)), |
| KASKO: fmt(b * (0.82 + Math.random() * 0.14)), |
| ASISTENCA: fmt(b * (0.09 + Math.random() * 0.04)), |
| }; |
| } |
| |
| function fmt(v) { return '€' + v.toFixed(2).replace('.', ','); } |
| |
| |
| async function runDOM(insurer) { |
| if (state.isRunning) { log('⚠️ Scrape already in progress', 'warn'); return; } |
| |
| const profiles = state.allProfiles ? ['car-1', 'car-2', 'car-3', 'car-4'] : [state.activeProfile]; |
| const insurers = insurer === 'all' ? ['hi3', 'wiz', 'g24', 'zt'] : [insurer]; |
| |
| if (!document.getElementById('debugMode').checked) { |
| log('🐛 Debug mode OFF — browser will run headless', 'browser'); |
| } |
| |
| state.isRunning = true; |
| setButtonState(true); |
| const total = profiles.length * insurers.length; |
| let completed = 0; |
| |
| updateProgress(0); |
| log(`═══ Playwright: ${insurers.length} insurer(s) × ${profiles.length} profile(s) ═══`, 'highlight'); |
| |
| for (const ins of insurers) { |
| setInsurerStatus(ins, 'running'); |
| for (const prof of profiles) { |
| const v = VEHICLES[prof]; |
| log(`── ${ins.toUpperCase()} → ${v.name} (${v.plate}) ──`, 'highlight'); |
| try { |
| const premiums = await playWrightDOMAdapter(ins, prof); |
| if (!state.results[prof]) state.results[prof] = {}; |
| state.results[prof][ins] = premiums; |
| completed++; |
| updateProgress(Math.round((completed / total) * 100)); |
| } catch (e) { |
| log(`[${ins.toUpperCase()}] ❌ DOM scrape failed: ${e.message}`, 'error'); |
| if (!state.results[prof]) state.results[prof] = {}; |
| state.results[prof][ins] = { AO: '—', AO_PLUS: '—', KASKO: '—', ASISTENCA: '—' }; |
| completed++; |
| } |
| } |
| setInsurerStatus(ins, 'done'); |
| } |
| |
| updateProgress(100); |
| log('✅ All Playwright scrapes completed!', 'success'); |
| state.isRunning = false; |
| setButtonState(false); |
| refreshDashboard(); |
| updateSummaryStats(); |
| setTimeout(() => updateProgress(0), 1500); |
| } |
| |
| |
| function refreshDashboard() { |
| const tbody = document.getElementById('tableBody'); |
| const profiles = ['car-1', 'car-2', 'car-3', 'car-4']; |
| const insurers = ['hi3', 'wiz', 'g24', 'zt']; |
| const covs = ['AO', 'AO_PLUS', 'KASKO', 'ASISTENCA']; |
| let rows = ''; |
| |
| for (const prof of profiles) { |
| const v = VEHICLES[prof]; |
| const data = state.results[prof] || {}; |
| rows += '<tr>'; |
| rows += `<td><strong>${v.name}</strong></td>`; |
| rows += `<td>${v.plate}</td>`; |
| for (const ins of insurers) { |
| const d = data[ins] || {}; |
| for (const c of covs) { |
| const val = d[c]; |
| rows += `<td class="${val && val !== '—' && !val.startsWith('⚠️') ? 'price' : 'price missing'}">${val || '—'}</td>`; |
| } |
| } |
| rows += '</tr>'; |
| } |
| |
| if (Object.keys(state.results).length === 0) { |
| tbody.innerHTML = '<tr><td colspan="18" style="text-align:center;color:#8b949e;padding:30px;">Run a scrape to see premium data</td></tr>'; |
| } else { |
| tbody.innerHTML = rows; |
| } |
| |
| document.getElementById('lastUpdated').textContent = new Date().toLocaleString('sl-SI'); |
| updateSummaryStats(); |
| } |
| |
| function updateSummaryStats() { |
| let insurerCount = 0, quoteCount = 0, totalVal = 0, qvc = 0; |
| for (const prof in state.results) { |
| for (const ins in state.results[prof]) { |
| insurerCount = Math.max(insurerCount, Object.keys(state.results[prof]).length); |
| for (const c of ['AO', 'AO_PLUS', 'KASKO', 'ASISTENCA']) { |
| const val = state.results[prof][ins][c]; |
| if (val && val !== '—' && !val.startsWith('⚠️')) { |
| quoteCount++; |
| const n = parseFloat(val.replace('€', '').replace(',', '.')); |
| if (!isNaN(n)) { totalVal += n; qvc++; } |
| } |
| } |
| } |
| } |
| document.getElementById('statInsurers').textContent = insurerCount; |
| document.getElementById('statQuotes').textContent = quoteCount; |
| document.getElementById('statTotal').textContent = qvc > 0 ? '€' + (totalVal / qvc).toFixed(2).replace('.', ',') : '€0'; |
| } |
| |
| |
| function exportCSV() { |
| const profiles = ['car-1', 'car-2', 'car-3', 'car-4']; |
| const insurers = ['hi3', 'wiz', 'g24', 'zt']; |
| const covs = ['AO', 'AO_PLUS', 'KASKO', 'ASISTENCA']; |
| |
| let csv = 'car_type,'; |
| for (const ins of insurers) for (const c of covs) csv += `${ins}_${c},`; |
| csv = csv.slice(0, -1) + '\n'; |
| |
| for (const prof of profiles) { |
| csv += `${prof},`; |
| const data = state.results[prof] || {}; |
| for (const ins of insurers) { |
| const d = data[ins] || {}; |
| for (const c of covs) csv += `"${d[c] || ''}",`; |
| } |
| csv = csv.slice(0, -1) + '\n'; |
| } |
| |
| const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); |
| const link = document.createElement('a'); |
| link.href = URL.createObjectURL(blob); |
| link.download = `premiums-${new Date().toISOString().slice(0, 10)}.csv`; |
| link.click(); |
| |
| addExportLog(`✅ Exported premiums-${new Date().toISOString().slice(0, 10)}.csv (16 columns, 4 rows)`); |
| } |
| |
| function publishHereNow() { |
| addExportLog('🚀 Publishing static comparison page to here.now...'); |
| setTimeout(() => { |
| addExportLog('✅ Published: https://gilded-delta-nm6c.here.now/'); |
| document.getElementById('publishUrl').textContent = 'https://gilded-delta-nm6c.here.now/'; |
| document.getElementById('publishUrl').href = 'https://gilded-delta-nm6c.here.now/'; |
| }, 800); |
| } |
| |
| |
| function log(msg, type = 'info') { |
| state.logs.push(msg); |
| const panel = document.getElementById('logPanel'); |
| const line = document.createElement('div'); |
| line.className = `log-line ${type}`; |
| line.textContent = `[${new Date().toLocaleTimeString('sl-SI')}] ${msg}`; |
| panel.appendChild(line); |
| panel.scrollTop = panel.scrollHeight; |
| document.getElementById('logStatus').textContent = `(${state.logs.length} lines)`; |
| } |
| |
| function addExportLog(msg) { |
| const panel = document.getElementById('exportLogPanel'); |
| const line = document.createElement('div'); |
| line.className = 'log-line info'; |
| line.textContent = `[${new Date().toLocaleTimeString('sl-SI')}] ${msg}`; |
| panel.appendChild(line); |
| panel.scrollTop = panel.scrollHeight; |
| } |
| |
| function setInsurerStatus(insurer, status) { |
| state.insurerStatus[insurer] = status; |
| const chip = document.querySelector(`.insurer-chip[data-insurer="${insurer}"]`); |
| if (!chip) return; |
| chip.querySelector('.status-dot').className = `status-dot ${status}`; |
| chip.className = `insurer-chip ${status}`; |
| } |
| |
| function setButtonState(disabled) { |
| document.querySelectorAll('.btn-primary, .btn-danger').forEach(b => b.disabled = disabled); |
| } |
| |
| function updateProgress(pct) { |
| document.getElementById('progressFill').style.width = pct + '%'; |
| } |
| |
| function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } |
| |
| |
| document.querySelectorAll('.profile-btn').forEach(btn => { |
| btn.addEventListener('click', () => { |
| document.querySelectorAll('.profile-btn').forEach(b => b.classList.remove('active')); |
| btn.classList.add('active'); |
| state.activeProfile = btn.dataset.profile; |
| }); |
| }); |
| |
| document.getElementById('allProfiles').addEventListener('change', e => { |
| state.allProfiles = e.target.checked; |
| log(state.allProfiles ? '📦 Batch mode: ALL 4 profiles' : '🎯 Single profile mode', 'highlight'); |
| }); |
| |
| document.querySelectorAll('.tab').forEach(tab => { |
| tab.addEventListener('click', () => { |
| document.querySelectorAll('.tab, .tab-content').forEach(el => el.classList.remove('active')); |
| tab.classList.add('active'); |
| document.getElementById(`tab-${tab.dataset.tab}`).classList.add('active'); |
| if (tab.dataset.tab === 'dashboard') refreshDashboard(); |
| }); |
| }); |
| |
| refreshDashboard(); |
| </script> |
|
|
| </body> |
| </html> |