| <!DOCTYPE html> |
| <html lang="en"> |
|
|
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>ConstructScan β Illegal Construction Detector</title> |
| <link |
| href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap" |
| rel="stylesheet" /> |
| <style> |
| :root { |
| --bg: #0a0a0a; |
| --surface: #111; |
| --surface2: #1a1a1a; |
| --border: #2a2a2a; |
| --accent: #ff3c00; |
| --text: #f0ede8; |
| --muted: #666; |
| --safe: #00e676; |
| --danger: #ff3c00; |
| } |
|
|
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
|
|
| body { |
| background: var(--bg); |
| color: var(--text); |
| font-family: 'DM Sans', sans-serif; |
| min-height: 100vh; |
| } |
|
|
| body::before { |
| content: ''; |
| position: fixed; |
| inset: 0; |
| pointer-events: none; |
| z-index: 999; |
| background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E"); |
| } |
|
|
| header { |
| border-bottom: 1px solid var(--border); |
| padding: 1.2rem 2.5rem; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| position: sticky; |
| top: 0; |
| background: rgba(10, 10, 10, 0.96); |
| backdrop-filter: blur(12px); |
| z-index: 100; |
| } |
|
|
| .logo { |
| font-family: 'Bebas Neue', sans-serif; |
| font-size: 1.7rem; |
| letter-spacing: .1em; |
| } |
|
|
| .logo span { |
| color: var(--accent); |
| } |
|
|
| .badge { |
| font-family: 'DM Mono', monospace; |
| font-size: .65rem; |
| letter-spacing: .15em; |
| text-transform: uppercase; |
| padding: .3rem .7rem; |
| border: 1px solid var(--border); |
| color: var(--muted); |
| } |
|
|
| main { |
| max-width: 1100px; |
| margin: 0 auto; |
| padding: 3.5rem 2rem; |
| } |
|
|
| .hero { |
| margin-bottom: 2.5rem; |
| } |
|
|
| .hero h1 { |
| font-family: 'Bebas Neue', sans-serif; |
| font-size: clamp(2.8rem, 7vw, 6rem); |
| line-height: .92; |
| letter-spacing: .02em; |
| margin-bottom: 1rem; |
| } |
|
|
| .hero h1 em { |
| font-style: normal; |
| color: var(--accent); |
| display: block; |
| } |
|
|
| .hero p { |
| font-size: .95rem; |
| color: var(--muted); |
| max-width: 460px; |
| line-height: 1.7; |
| font-weight: 300; |
| } |
|
|
| /* Tabs */ |
| .tabs { |
| display: flex; |
| gap: 1px; |
| margin-bottom: 1px; |
| background: var(--border); |
| } |
|
|
| .tab { |
| flex: 1; |
| padding: .9rem 1.5rem; |
| background: var(--surface2); |
| cursor: pointer; |
| font-family: 'DM Mono', monospace; |
| font-size: .7rem; |
| letter-spacing: .15em; |
| text-transform: uppercase; |
| color: var(--muted); |
| border: none; |
| transition: all .2s; |
| text-align: center; |
| } |
|
|
| .tab.active { |
| background: var(--surface); |
| color: var(--text); |
| border-bottom: 2px solid var(--accent); |
| } |
|
|
| .tab:hover:not(.active) { |
| color: var(--text); |
| background: #161616; |
| } |
|
|
| .tab-panel { |
| display: none; |
| } |
|
|
| .tab-panel.active { |
| display: block; |
| } |
|
|
| /* Upload tab */ |
| .upload-zone { |
| border: 1px dashed var(--border); |
| padding: 2.5rem 2rem; |
| text-align: center; |
| cursor: pointer; |
| background: var(--surface); |
| position: relative; |
| transition: all .2s; |
| margin-bottom: 1rem; |
| } |
|
|
| .upload-zone:hover, |
| .upload-zone.drag-over { |
| border-color: var(--accent); |
| background: #1a0906; |
| } |
|
|
| .upload-zone input { |
| position: absolute; |
| inset: 0; |
| opacity: 0; |
| cursor: pointer; |
| width: 100%; |
| height: 100%; |
| } |
|
|
| .upload-zone svg { |
| width: 40px; |
| height: 40px; |
| margin: 0 auto .8rem; |
| opacity: .35; |
| display: block; |
| } |
|
|
| .upload-zone h3 { |
| font-family: 'DM Mono', monospace; |
| font-size: .8rem; |
| letter-spacing: .1em; |
| text-transform: uppercase; |
| color: var(--muted); |
| margin-bottom: .4rem; |
| } |
|
|
| .upload-zone p { |
| font-size: .75rem; |
| color: #444; |
| } |
|
|
| #preview-wrap { |
| display: none; |
| margin-bottom: 1rem; |
| position: relative; |
| } |
|
|
| #preview-img { |
| width: 100%; |
| max-height: 280px; |
| object-fit: cover; |
| display: block; |
| } |
|
|
| .preview-tag { |
| position: absolute; |
| top: .8rem; |
| left: .8rem; |
| font-family: 'DM Mono', monospace; |
| font-size: .6rem; |
| letter-spacing: .15em; |
| text-transform: uppercase; |
| background: rgba(0, 0, 0, .85); |
| padding: .25rem .55rem; |
| color: var(--muted); |
| } |
|
|
| /* Map tab */ |
| .map-instructions { |
| padding: .8rem 1rem; |
| background: var(--surface2); |
| border-left: 3px solid var(--accent); |
| font-family: 'DM Mono', monospace; |
| font-size: .7rem; |
| letter-spacing: .08em; |
| color: var(--muted); |
| margin-bottom: 1rem; |
| } |
|
|
| .map-instructions strong { |
| color: var(--text); |
| } |
|
|
| #map-container { |
| position: relative; |
| margin-bottom: 1rem; |
| } |
|
|
| #map { |
| width: 100%; |
| height: 480px; |
| background: #111; |
| } |
|
|
| .map-crosshair { |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| pointer-events: none; |
| z-index: 10; |
| } |
|
|
| .map-crosshair svg { |
| width: 40px; |
| height: 40px; |
| filter: drop-shadow(0 0 4px rgba(0, 0, 0, .8)); |
| } |
|
|
| .map-info-bar { |
| display: flex; |
| gap: 1px; |
| background: var(--border); |
| margin-bottom: 1rem; |
| } |
|
|
| .map-info-cell { |
| flex: 1; |
| background: var(--surface2); |
| padding: .8rem 1rem; |
| } |
|
|
| .map-info-cell .k { |
| font-family: 'DM Mono', monospace; |
| font-size: .6rem; |
| letter-spacing: .12em; |
| text-transform: uppercase; |
| color: var(--muted); |
| margin-bottom: .3rem; |
| } |
|
|
| .map-info-cell .v { |
| font-family: 'DM Mono', monospace; |
| font-size: .85rem; |
| color: var(--text); |
| } |
|
|
| .zoom-note { |
| font-family: 'DM Mono', monospace; |
| font-size: .65rem; |
| color: #444; |
| letter-spacing: .08em; |
| margin-bottom: 1rem; |
| } |
|
|
| /* Shared */ |
| .btn { |
| width: 100%; |
| padding: 1.1rem; |
| background: var(--accent); |
| color: #fff; |
| border: none; |
| font-family: 'Bebas Neue', sans-serif; |
| font-size: 1.3rem; |
| letter-spacing: .15em; |
| cursor: pointer; |
| transition: background .2s; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: .7rem; |
| } |
|
|
| .btn:hover { |
| background: #e03500; |
| } |
|
|
| .btn:disabled { |
| background: #333; |
| color: #555; |
| cursor: not-allowed; |
| } |
|
|
| .btn.secondary { |
| background: var(--surface2); |
| color: var(--text); |
| border: 1px solid var(--border); |
| margin-bottom: 1rem; |
| } |
|
|
| .btn.secondary:hover { |
| background: #222; |
| } |
|
|
| .spinner { |
| width: 18px; |
| height: 18px; |
| border: 2px solid rgba(255, 255, 255, .25); |
| border-top-color: #fff; |
| border-radius: 50%; |
| animation: spin .7s linear infinite; |
| display: none; |
| } |
|
|
| @keyframes spin { |
| to { |
| transform: rotate(360deg); |
| } |
| } |
|
|
| #error { |
| display: none; |
| margin-top: .8rem; |
| padding: .9rem 1.2rem; |
| background: #1a0500; |
| border-left: 3px solid var(--danger); |
| font-family: 'DM Mono', monospace; |
| font-size: .75rem; |
| color: var(--danger); |
| } |
|
|
| /* Map preview */ |
| #map-preview-wrap { |
| display: none; |
| margin-bottom: 1rem; |
| position: relative; |
| } |
|
|
| #map-preview-img { |
| width: 100%; |
| max-height: 280px; |
| object-fit: cover; |
| display: block; |
| } |
|
|
| /* Results */ |
| #results { |
| display: none; |
| margin-top: 2.5rem; |
| animation: fadeUp .45s ease forwards; |
| } |
|
|
| @keyframes fadeUp { |
| from { |
| opacity: 0; |
| transform: translateY(18px); |
| } |
|
|
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
|
|
| .verdict-bar { |
| padding: 1.8rem 2rem; |
| margin-bottom: 1px; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| gap: 1.5rem; |
| flex-wrap: wrap; |
| } |
|
|
| .verdict-bar.danger { |
| background: #1a0400; |
| border-left: 4px solid var(--danger); |
| } |
|
|
| .verdict-bar.safe { |
| background: #001508; |
| border-left: 4px solid var(--safe); |
| } |
|
|
| .verdict-label { |
| font-family: 'Bebas Neue', sans-serif; |
| font-size: clamp(1.4rem, 3.5vw, 2.5rem); |
| letter-spacing: .04em; |
| } |
|
|
| .verdict-bar.danger .verdict-label { |
| color: var(--danger); |
| } |
|
|
| .verdict-bar.safe .verdict-label { |
| color: var(--safe); |
| } |
|
|
| .verdict-meta { |
| display: flex; |
| gap: 2rem; |
| } |
|
|
| .vmeta-item { |
| text-align: right; |
| } |
|
|
| .vmeta-item .num { |
| font-family: 'Bebas Neue', sans-serif; |
| font-size: 2.2rem; |
| line-height: 1; |
| } |
|
|
| .vmeta-item .key { |
| font-family: 'DM Mono', monospace; |
| font-size: .6rem; |
| letter-spacing: .12em; |
| text-transform: uppercase; |
| color: var(--muted); |
| } |
|
|
| .grid3 { |
| display: grid; |
| grid-template-columns: repeat(3, 1fr); |
| gap: 1px; |
| background: var(--border); |
| margin-bottom: 1px; |
| } |
|
|
| .img-panel { |
| background: var(--surface); |
| overflow: hidden; |
| } |
|
|
| .panel-label { |
| padding: .6rem 1rem; |
| font-family: 'DM Mono', monospace; |
| font-size: .6rem; |
| letter-spacing: .14em; |
| text-transform: uppercase; |
| color: var(--muted); |
| border-bottom: 1px solid var(--border); |
| display: flex; |
| align-items: center; |
| gap: .4rem; |
| } |
|
|
| .dot { |
| width: 5px; |
| height: 5px; |
| border-radius: 50%; |
| background: var(--accent); |
| } |
|
|
| .dot.g { |
| background: var(--safe); |
| } |
|
|
| .img-panel img { |
| width: 100%; |
| aspect-ratio: 1; |
| object-fit: cover; |
| display: block; |
| } |
|
|
| .stats4 { |
| display: grid; |
| grid-template-columns: repeat(4, 1fr); |
| gap: 1px; |
| background: var(--border); |
| } |
|
|
| .stat { |
| background: var(--surface2); |
| padding: 1.3rem 1.5rem; |
| } |
|
|
| .stat .v { |
| font-family: 'Bebas Neue', sans-serif; |
| font-size: 1.8rem; |
| color: var(--text); |
| line-height: 1; |
| margin-bottom: .25rem; |
| } |
|
|
| .stat .k { |
| font-family: 'DM Mono', monospace; |
| font-size: .6rem; |
| letter-spacing: .12em; |
| text-transform: uppercase; |
| color: var(--muted); |
| } |
|
|
| @media(max-width:700px) { |
| header { |
| padding: 1rem; |
| } |
|
|
| main { |
| padding: 2rem 1rem; |
| } |
|
|
| .grid3, |
| .stats4 { |
| grid-template-columns: 1fr; |
| } |
|
|
| .verdict-meta { |
| gap: 1rem; |
| } |
|
|
| #map { |
| height: 320px; |
| } |
| } |
| </style> |
| </head> |
|
|
| <body> |
|
|
| <header> |
| <div class="logo">Construct<span>Scan</span></div> |
| <div class="badge">EfficientNet-B3 Β· U-Net Β· RTX 4050</div> |
| </header> |
|
|
| <main> |
| <div class="hero"> |
| <h1>Detect <em>Illegal</em> Construction</h1> |
| <p>Upload a satellite image or pick any location on the map. The model segments buildings and flags unauthorized |
| construction.</p> |
| </div> |
|
|
| |
| <div class="tabs"> |
| <button class="tab active" onclick="switchTab('upload')">β¬ Upload Image</button> |
| <button class="tab" onclick="switchTab('map')">π Pick on Map</button> |
| </div> |
|
|
| |
| <div class="tab-panel active" id="panel-upload"> |
| <div class="upload-zone" id="upload-zone"> |
| <input type="file" id="file-input" accept="image/*" /> |
| <svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="1.5"> |
| <rect x="4" y="4" width="40" height="40" rx="2" /> |
| <path d="M24 32V16M16 24l8-8 8 8" /> |
| </svg> |
| <h3>Drop image here or click to upload</h3> |
| <p>Satellite / aerial imagery β JPG, PNG, TIFF</p> |
| </div> |
| <div id="preview-wrap"> |
| <img id="preview-img" src="" alt="Preview" /> |
| <span class="preview-tag">Input</span> |
| </div> |
| </div> |
|
|
| |
| <div class="tab-panel" id="panel-map"> |
| <div class="map-instructions"> |
| <strong>How to use:</strong> Navigate to any location β zoom in to building level β click <strong>Capture This |
| View</strong> |
| </div> |
| <div id="map-container"> |
| <div id="map"></div> |
| <div class="map-crosshair"> |
| <svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| <circle cx="20" cy="20" r="8" stroke="#ff3c00" stroke-width="2" /> |
| <line x1="20" y1="2" x2="20" y2="12" stroke="#ff3c00" stroke-width="2" /> |
| <line x1="20" y1="28" x2="20" y2="38" stroke="#ff3c00" stroke-width="2" /> |
| <line x1="2" y1="20" x2="12" y2="20" stroke="#ff3c00" stroke-width="2" /> |
| <line x1="28" y1="20" x2="38" y2="20" stroke="#ff3c00" stroke-width="2" /> |
| </svg> |
| </div> |
| </div> |
| <div class="map-info-bar"> |
| <div class="map-info-cell"> |
| <div class="k">Latitude</div> |
| <div class="v" id="info-lat">β</div> |
| </div> |
| <div class="map-info-cell"> |
| <div class="k">Longitude</div> |
| <div class="v" id="info-lng">β</div> |
| </div> |
| <div class="map-info-cell"> |
| <div class="k">Zoom Level</div> |
| <div class="v" id="info-zoom">β</div> |
| </div> |
| </div> |
| <p class="zoom-note">β Zoom in to at least level 17β19 for best building-level detection results</p> |
| <button class="btn secondary" id="capture-btn" onclick="captureMap()">πΈ CAPTURE THIS VIEW</button> |
| <div id="map-preview-wrap"> |
| <img id="map-preview-img" src="" alt="Captured map" /> |
| <span class="preview-tag">Captured Satellite View</span> |
| </div> |
| </div> |
|
|
| |
| <button class="btn" id="analyze-btn" disabled> |
| <div class="spinner" id="spinner"></div> |
| <span id="btn-text">ANALYZE IMAGE</span> |
| </button> |
|
|
| <div id="error"></div> |
|
|
| |
| <div id="results"> |
| <div class="verdict-bar" id="verdict-bar"> |
| <div class="verdict-label" id="verdict-label"></div> |
| <div class="verdict-meta"> |
| <div class="vmeta-item"> |
| <div class="num" id="vm-illegal">0</div> |
| <div class="key">Illegal Buildings</div> |
| </div> |
| <div class="vmeta-item"> |
| <div class="num" id="vm-total">0</div> |
| <div class="key">Total Buildings</div> |
| </div> |
| </div> |
| </div> |
| <div class="grid3"> |
| <div class="img-panel"> |
| <div class="panel-label"><span class="dot g"></span> Original</div> |
| <img id="out-orig" src="" alt="Original" /> |
| </div> |
| <div class="img-panel"> |
| <div class="panel-label"><span class="dot"></span> Segmentation Mask</div> |
| <img id="out-mask" src="" alt="Mask" /> |
| </div> |
| <div class="img-panel"> |
| <div class="panel-label"><span class="dot"></span> Illegal Overlay</div> |
| <img id="out-overlay" src="" alt="Overlay" /> |
| </div> |
| </div> |
| <div class="stats4"> |
| <div class="stat"> |
| <div class="v" id="s-illegal">β</div> |
| <div class="k">Illegal Buildings</div> |
| </div> |
| <div class="stat"> |
| <div class="v" id="s-legal">β</div> |
| <div class="k">Legal Buildings</div> |
| </div> |
| <div class="stat"> |
| <div class="v" id="s-pct">β</div> |
| <div class="k">Area Flagged %</div> |
| </div> |
| <div class="stat"> |
| <div class="v" id="s-device">β</div> |
| <div class="k">Inference Device</div> |
| </div> |
| </div> |
| </div> |
| </main> |
|
|
| <script> |
| const API_KEY = 'AIzaSyBhw53jmyYyAuhEwkZJ7ADjijxoWNruRTk'; |
| let map, currentLat = 28.6139, currentLng = 77.2090; |
| let currentZoom = 18; |
| let selectedFile = null; |
| let capturedBlob = null; |
| let activeSource = 'upload'; |
| |
| |
| function switchTab(tab) { |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); |
| document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active')); |
| document.querySelector(`.tab[onclick="switchTab('${tab}')"]`).classList.add('active'); |
| document.getElementById(`panel-${tab}`).classList.add('active'); |
| activeSource = tab; |
| |
| if (tab === 'map' && !map) initMap(); |
| updateAnalyzeBtn(); |
| } |
| |
| |
| function initMap() { |
| map = new google.maps.Map(document.getElementById('map'), { |
| center: { lat: currentLat, lng: currentLng }, |
| zoom: currentZoom, |
| mapTypeId: 'satellite', |
| tilt: 0, |
| disableDefaultUI: false, |
| mapTypeControl: false, |
| streetViewControl: false, |
| fullscreenControl: true, |
| zoomControl: true, |
| styles: [] |
| }); |
| |
| map.addListener('center_changed', updateMapInfo); |
| map.addListener('zoom_changed', updateMapInfo); |
| updateMapInfo(); |
| } |
| |
| function updateMapInfo() { |
| if (!map) return; |
| const c = map.getCenter(); |
| currentLat = c.lat(); |
| currentLng = c.lng(); |
| currentZoom = map.getZoom(); |
| document.getElementById('info-lat').textContent = currentLat.toFixed(6); |
| document.getElementById('info-lng').textContent = currentLng.toFixed(6); |
| document.getElementById('info-zoom').textContent = currentZoom; |
| } |
| |
| |
| function captureMap() { |
| if (!map) return; |
| const c = map.getCenter(); |
| const lat = c.lat(); |
| const lng = c.lng(); |
| const zoom = map.getZoom(); |
| const size = '640x640'; |
| |
| const url = `https://maps.googleapis.com/maps/api/staticmap?center=${lat},${lng}&zoom=${zoom}&size=${size}&maptype=satellite&key=${API_KEY}`; |
| |
| document.getElementById('map-preview-img').src = url; |
| document.getElementById('map-preview-wrap').style.display = 'block'; |
| |
| |
| fetch(url) |
| .then(r => r.blob()) |
| .then(blob => { |
| capturedBlob = blob; |
| activeSource = 'map'; |
| updateAnalyzeBtn(); |
| }) |
| .catch(() => { |
| showError('Failed to fetch satellite image. Check your API key or network.'); |
| }); |
| } |
| |
| |
| const fileInput = document.getElementById('file-input'); |
| const uploadZone = document.getElementById('upload-zone'); |
| |
| uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('drag-over'); }); |
| uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('drag-over')); |
| uploadZone.addEventListener('drop', e => { |
| e.preventDefault(); uploadZone.classList.remove('drag-over'); |
| if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]); |
| }); |
| fileInput.addEventListener('change', () => { if (fileInput.files[0]) handleFile(fileInput.files[0]); }); |
| |
| function handleFile(file) { |
| selectedFile = file; |
| activeSource = 'upload'; |
| const r = new FileReader(); |
| r.onload = e => { |
| document.getElementById('preview-img').src = e.target.result; |
| document.getElementById('preview-wrap').style.display = 'block'; |
| }; |
| r.readAsDataURL(file); |
| updateAnalyzeBtn(); |
| document.getElementById('results').style.display = 'none'; |
| document.getElementById('error').style.display = 'none'; |
| } |
| |
| function updateAnalyzeBtn() { |
| const btn = document.getElementById('analyze-btn'); |
| const hasUpload = activeSource === 'upload' && selectedFile; |
| const hasMap = activeSource === 'map' && capturedBlob; |
| btn.disabled = !(hasUpload || hasMap); |
| } |
| |
| |
| document.getElementById('analyze-btn').addEventListener('click', async () => { |
| const btn = document.getElementById('analyze-btn'); |
| const spinner = document.getElementById('spinner'); |
| const btnText = document.getElementById('btn-text'); |
| |
| btn.disabled = true; |
| spinner.style.display = 'block'; |
| btnText.textContent = 'ANALYZING...'; |
| document.getElementById('error').style.display = 'none'; |
| document.getElementById('results').style.display = 'none'; |
| |
| const fd = new FormData(); |
| if (activeSource === 'upload' && selectedFile) { |
| fd.append('image', selectedFile); |
| } else if (activeSource === 'map' && capturedBlob) { |
| fd.append('image', capturedBlob, 'satellite_capture.png'); |
| } else { |
| showError('No image selected.'); return; |
| } |
| |
| try { |
| const res = await fetch('/predict', { method: 'POST', body: fd }); |
| const d = await res.json(); |
| if (d.error) throw new Error(d.error); |
| |
| const isIllegal = d.illegal_count > 0; |
| const bar = document.getElementById('verdict-bar'); |
| bar.className = 'verdict-bar ' + (isIllegal ? 'danger' : 'safe'); |
| document.getElementById('verdict-label').textContent = d.verdict; |
| document.getElementById('vm-illegal').textContent = d.illegal_count; |
| document.getElementById('vm-total').textContent = d.total_count; |
| |
| document.getElementById('out-orig').src = 'data:image/png;base64,' + d.original; |
| document.getElementById('out-mask').src = 'data:image/png;base64,' + d.mask; |
| document.getElementById('out-overlay').src = 'data:image/png;base64,' + d.overlay; |
| |
| document.getElementById('s-illegal').textContent = d.illegal_count; |
| document.getElementById('s-legal').textContent = d.legal_count; |
| document.getElementById('s-pct').textContent = d.illegal_percent + '%'; |
| document.getElementById('s-device').textContent = d.device.toUpperCase(); |
| |
| document.getElementById('results').style.display = 'block'; |
| document.getElementById('results').scrollIntoView({ behavior: 'smooth' }); |
| } catch (err) { |
| showError(err.message || 'Server error. Is Flask running?'); |
| } finally { |
| btn.disabled = false; |
| spinner.style.display = 'none'; |
| btnText.textContent = 'ANALYZE AGAIN'; |
| updateAnalyzeBtn(); |
| } |
| }); |
| |
| function showError(msg) { |
| const e = document.getElementById('error'); |
| e.textContent = 'β ' + msg; |
| e.style.display = 'block'; |
| document.getElementById('spinner').style.display = 'none'; |
| document.getElementById('btn-text').textContent = 'ANALYZE AGAIN'; |
| document.getElementById('analyze-btn').disabled = false; |
| updateAnalyzeBtn(); |
| } |
| </script> |
|
|
| |
| <script |
| src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBhw53jmyYyAuhEwkZJ7ADjijxoWNruRTk&callback=Function.prototype" |
| async defer></script> |
|
|
| </body> |
|
|
| </html> |