| """ |
| 🛰️ Spy Satellite Simulator — Globe via base64 iframe |
| """ |
| import gradio as gr |
| import time, threading, logging, json, os, base64 |
| from datetime import datetime |
| from data_fetcher import DataManager |
|
|
| logging.basicConfig(level=logging.INFO) |
| log = logging.getLogger("app") |
|
|
| CESIUM_TOKEN = os.environ.get("CESIUM_TOKEN", |
| "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJmMzQ3OTJjZi01NmI0LTQ2MDItOWM1MC05ZWJjNWFlMDNiZTciLCJpZCI6MTkzMzUwLCJpYXQiOjE3NzQ4ODQ1OTR9.9AvXB2qj1fxNQzll4GVfKsqQtFBtpkGV9zUAedZe_fs") |
|
|
| dm = DataManager() |
|
|
| |
| log.info("🚀 Starting initial data fetch...") |
| dm.fetch_all() |
| log.info("🚀 Initial fetch complete!") |
|
|
| |
| def bg_fetch(): |
| while True: |
| time.sleep(300) |
| try: dm.fetch_all() |
| except Exception as e: log.error(f"BG: {e}") |
|
|
| threading.Thread(target=bg_fetch, daemon=True).start() |
|
|
|
|
| |
| def make_globe_html(data_json): |
| return '''<!DOCTYPE html> |
| <html><head><meta charset="utf-8"> |
| <script src="https://cesium.com/downloads/cesiumjs/releases/1.119/Build/Cesium/Cesium.js"></script> |
| <link href="https://cesium.com/downloads/cesiumjs/releases/1.119/Build/Cesium/Widgets/widgets.css" rel="stylesheet"> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap'); |
| *{margin:0;padding:0;box-sizing:border-box} |
| html,body{width:100%;height:100%;overflow:hidden;background:#050a14;font-family:'Cairo',sans-serif;color:#e2e8f0} |
| #cC{width:100%;height:100%;position:absolute;top:0;left:0} |
| .cesium-viewer-bottom,.cesium-viewer-toolbar,.cesium-viewer-fullscreenContainer,.cesium-viewer .cesium-widget-credits{display:none!important} |
| .hud{position:absolute;z-index:999;pointer-events:none} |
| .hp{pointer-events:auto} |
| .tb{top:0;left:0;right:0;height:42px;display:flex;align-items:center;justify-content:space-between;padding:0 14px;background:rgba(5,10,20,0.92);border-bottom:1px solid rgba(14,165,233,0.2)} |
| .logo{font-weight:800;font-size:14px;background:linear-gradient(90deg,#e2e8f0,#0ea5e9);-webkit-background-clip:text;-webkit-text-fill-color:transparent} |
| .st{display:flex;gap:14px;font-size:11px;color:#94a3b8;direction:ltr} |
| .sv{color:#0ea5e9;font-weight:700;font-family:'JetBrains Mono',monospace} |
| .lb{display:flex;align-items:center;gap:5px;background:rgba(239,68,68,0.12);border:1px solid rgba(239,68,68,0.3);padding:3px 10px;border-radius:14px;color:#ef4444;font-size:10px;font-weight:700} |
| .ld{width:6px;height:6px;border-radius:50%;background:#ef4444;animation:p 1.5s infinite} |
| .rp{top:50px;right:6px;width:310px;max-height:calc(100vh-58px);overflow:hidden;display:flex;flex-direction:column;background:rgba(5,10,20,0.88);border:1px solid rgba(14,165,233,0.2);border-radius:8px;backdrop-filter:blur(10px)} |
| .pt{display:flex;border-bottom:1px solid rgba(14,165,233,0.15)} |
| .ptb{flex:1;padding:6px;text-align:center;font-size:10px;font-weight:600;color:#64748b;cursor:pointer;border-bottom:2px solid transparent;pointer-events:auto} |
| .ptb:hover{color:#94a3b8}.ptb.a{color:#0ea5e9;border-bottom-color:#0ea5e9} |
| .pb{flex:1;overflow-y:auto;padding:6px;direction:rtl} |
| .pb::-webkit-scrollbar{width:3px}.pb::-webkit-scrollbar-thumb{background:#2a3a4e;border-radius:3px} |
| .cd{background:rgba(15,23,42,0.6);border:1px solid rgba(42,58,78,0.4);border-radius:6px;padding:8px;margin-bottom:4px;cursor:pointer;font-size:11px} |
| .cd:hover{border-color:rgba(14,165,233,0.3)} |
| .cd .t{font-weight:700;font-size:12px;margin-bottom:2px;line-height:1.5} |
| .cd .m{color:#64748b;font-size:9px;display:flex;gap:8px;flex-wrap:wrap} |
| .bg{padding:1px 6px;border-radius:3px;font-size:8px;font-weight:700} |
| .br{background:rgba(239,68,68,0.2);color:#ef4444} |
| .bo{background:rgba(249,115,22,0.2);color:#f97316} |
| .bc{background:rgba(6,182,212,0.2);color:#06b6d4} |
| .bg2{background:rgba(34,197,94,0.2);color:#22c55e} |
| .lc{top:50px;left:6px;display:flex;flex-direction:column;gap:3px} |
| .cb{width:32px;height:32px;background:rgba(5,10,20,0.85);border:1px solid rgba(14,165,233,0.2);border-radius:5px;color:#94a3b8;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:12px;pointer-events:auto} |
| .cb:hover{border-color:#0ea5e9;color:#0ea5e9}.cb.on{background:rgba(14,165,233,0.12);color:#0ea5e9;border-color:#0ea5e9} |
| .bb{bottom:0;left:0;right:0;height:28px;display:flex;align-items:center;justify-content:space-between;padding:0 12px;background:rgba(5,10,20,0.92);border-top:1px solid rgba(14,165,233,0.12);font-size:9px;color:#64748b;pointer-events:auto} |
| .ch{top:50%;left:50%;transform:translate(-50%,-50%);width:24px;height:24px} |
| .ch::before,.ch::after{content:'';position:absolute;background:rgba(14,165,233,0.2)} |
| .ch::before{width:1px;height:100%;left:50%}.ch::after{height:1px;width:100%;top:50%} |
| .tk{top:42px;left:0;right:0;height:22px;background:rgba(239,68,68,0.04);border-bottom:1px solid rgba(239,68,68,0.08);overflow:hidden;display:flex;align-items:center} |
| .tkt{white-space:nowrap;animation:sc 50s linear infinite;color:#ef4444;font-size:10px;font-weight:600} |
| @keyframes p{0%,100%{opacity:1}50%{opacity:0.3}} |
| @keyframes sc{0%{transform:translateX(100%)}100%{transform:translateX(-300%)}} |
| </style></head><body> |
| <div id="cC"></div> |
| <div class="hud tb hp"> |
| <div class="logo">🛰️ نظام المراقبة الفضائية</div> |
| <div class="st"><span>✈️ <span class="sv" id="sA">0</span></span><span>🚢 <span class="sv" id="sS">0</span></span><span>⚔️ <span class="sv" id="sC">0</span></span><span>📰 <span class="sv" id="sN">0</span></span></div> |
| <div style="display:flex;align-items:center;gap:8px"><div class="lb"><span class="ld"></span>LIVE</div><span style="color:#64748b;font-size:9px;font-family:'JetBrains Mono'" id="clk"></span></div> |
| </div> |
| <div class="hud tk"><div class="tkt" id="tkr">⏳ جارٍ التحميل...</div></div> |
| <div class="hud lc"> |
| <div class="cb on" onclick="tgl('mil')" id="bM">✈️</div> |
| <div class="cb on" onclick="tgl('civ')" id="bC">🛩️</div> |
| <div class="cb on" onclick="tgl('sh')" id="bS">🚢</div> |
| <div class="cb on" onclick="tgl('ev')" id="bE">⚔️</div> |
| <div class="cb" onclick="tgl('cm')" id="bCm">📷</div> |
| <div class="cb" onclick="v.camera.flyTo({destination:Cesium.Cartesian3.fromDegrees(40,28,8e6)})" >🎯</div> |
| </div> |
| <div class="hud ch"></div> |
| <div class="hud rp hp"> |
| <div class="pt"> |
| <div class="ptb a" onclick="stb('ev',this)">⚔️ أحداث</div> |
| <div class="ptb" onclick="stb('fl',this)">✈️ طيران</div> |
| <div class="ptb" onclick="stb('nw',this)">📰 أخبار</div> |
| <div class="ptb" onclick="stb('sr',this)">📡 مصادر</div> |
| </div> |
| <div class="pb" id="pb"><div style="text-align:center;padding:20px;color:#64748b">⏳</div></div> |
| </div> |
| <div class="hud bb hp"><div>🛰️ Spy Satellite — Real Data</div><div id="upd"></div></div> |
| |
| <script> |
| Cesium.Ion.defaultAccessToken='TOKEN_PLACEHOLDER'; |
| const v=new Cesium.Viewer('cC',{terrain:Cesium.Terrain.fromWorldTerrain(),baseLayerPicker:false,geocoder:false,homeButton:false,sceneModePicker:false,navigationHelpButton:false,animation:false,timeline:false,fullscreenButton:false,selectionIndicator:true,infoBox:true,shadows:false}); |
| v.scene.globe.enableLighting=true;v.scene.fog.enabled=true;v.scene.globe.showGroundAtmosphere=true;v.scene.backgroundColor=Cesium.Color.fromCssColorString('#050a14'); |
| v.imageryLayers.removeAll();v.imageryLayers.addImageryProvider(new Cesium.IonImageryProvider({assetId:3})); |
| const il=v.imageryLayers.get(0);il.brightness=0.4;il.contrast=1.3;il.saturation=0.35; |
| v.camera.flyTo({destination:Cesium.Cartesian3.fromDegrees(40,28,8e6),duration:0}); |
| |
| const D=DATA_PLACEHOLDER; |
| const L={mil:true,civ:true,sh:true,ev:true,cm:false}; |
| const G={mil:[],civ:[],sh:[],ev:[],cm:[]}; |
| |
| function tgl(n){L[n]=!L[n];const ids={mil:'bM',civ:'bC',sh:'bS',ev:'bE',cm:'bCm'};document.getElementById(ids[n]).classList.toggle('on',L[n]);G[n].forEach(e=>{e.show=L[n]})} |
| function stb(t,el){document.querySelectorAll('.ptb').forEach(x=>x.classList.remove('a'));el.classList.add('a');rp(t)} |
| function fly(lng,lat,alt){v.camera.flyTo({destination:Cesium.Cartesian3.fromDegrees(lng,lat,alt||1.5e6),duration:1.5})} |
| |
| // Military aircraft |
| (D.aircraft.military||[]).forEach(a=>{const e=v.entities.add({position:Cesium.Cartesian3.fromDegrees(a.lng,a.lat,a.alt),point:{pixelSize:7,color:Cesium.Color.RED,outlineColor:Cesium.Color.fromAlpha(Cesium.Color.RED,0.3),outlineWidth:8},label:{text:a.callsign,font:'11px JetBrains Mono',fillColor:Cesium.Color.fromCssColorString('#ef4444'),pixelOffset:new Cesium.Cartesian2(0,-12),scale:0.85,showBackground:true,backgroundColor:Cesium.Color.fromCssColorString('rgba(5,10,20,0.85)'),backgroundPadding:new Cesium.Cartesian2(5,2),distanceDisplayCondition:new Cesium.DistanceDisplayCondition(0,2.5e6)},description:'<div style="direction:rtl;font-family:Cairo"><h3 style="color:#ef4444">✈️ '+a.callsign+'</h3><p>ICAO: '+a.icao24+'</p><p>Alt: '+Math.round(a.alt)+'m | Spd: '+Math.round(a.velocity)+'m/s | Hdg: '+Math.round(a.heading)+'°</p><p style="color:#ef4444;font-weight:bold">عسكري</p></div>'});G.mil.push(e)}); |
| |
| // Civil aircraft |
| (D.aircraft.civil||[]).slice(0,300).forEach(a=>{const e=v.entities.add({position:Cesium.Cartesian3.fromDegrees(a.lng,a.lat,a.alt),point:{pixelSize:3,color:Cesium.Color.fromCssColorString('#06b6d4'),outlineWidth:2,outlineColor:Cesium.Color.fromAlpha(Cesium.Color.CYAN,0.1)},label:{text:a.callsign,font:'9px JetBrains Mono',fillColor:Cesium.Color.fromCssColorString('#64748b'),pixelOffset:new Cesium.Cartesian2(0,-8),scale:0.6,showBackground:true,backgroundColor:Cesium.Color.fromCssColorString('rgba(5,10,20,0.5)'),distanceDisplayCondition:new Cesium.DistanceDisplayCondition(0,5e5)}});G.civ.push(e)}); |
| |
| // Ships |
| (D.ships||[]).forEach(s=>{const e=v.entities.add({position:Cesium.Cartesian3.fromDegrees(s.lng,s.lat,0),point:{pixelSize:4,color:Cesium.Color.fromCssColorString('#22c55e'),outlineWidth:4,outlineColor:Cesium.Color.fromAlpha(Cesium.Color.GREEN,0.12)},label:{text:s.name||s.mmsi,font:'9px Cairo',fillColor:Cesium.Color.fromCssColorString('#22c55e'),pixelOffset:new Cesium.Cartesian2(0,-8),scale:0.65,showBackground:true,backgroundColor:Cesium.Color.fromCssColorString('rgba(5,10,20,0.6)'),distanceDisplayCondition:new Cesium.DistanceDisplayCondition(0,1e6)},description:'<div style="direction:rtl"><h3 style="color:#22c55e">🚢 '+s.name+'</h3><p>MMSI: '+s.mmsi+' | Spd: '+s.speed+' | Crs: '+s.course+'°</p></div>'});G.sh.push(e)}); |
| |
| // Conflicts |
| const cc={explosion:Cesium.Color.RED,battle:Cesium.Color.ORANGE,protest:Cesium.Color.YELLOW,strategic:Cesium.Color.PURPLE,violence:Cesium.Color.ORANGERED}; |
| (D.conflicts||[]).forEach(ev=>{const c=cc[ev.event_type]||Cesium.Color.RED;const e=v.entities.add({position:Cesium.Cartesian3.fromDegrees(ev.lng,ev.lat,0),point:{pixelSize:9,color:c,outlineWidth:12,outlineColor:Cesium.Color.fromAlpha(c,0.12)},ellipse:{semiMajorAxis:25000,semiMinorAxis:25000,material:Cesium.Color.fromAlpha(c,0.06),outline:true,outlineColor:Cesium.Color.fromAlpha(c,0.25)},label:{text:ev.title.substring(0,35),font:'10px Cairo',fillColor:c,pixelOffset:new Cesium.Cartesian2(0,-14),scale:0.8,showBackground:true,backgroundColor:Cesium.Color.fromCssColorString('rgba(5,10,20,0.85)'),backgroundPadding:new Cesium.Cartesian2(4,2),distanceDisplayCondition:new Cesium.DistanceDisplayCondition(0,2.5e6)},description:'<div style="direction:rtl"><h3>'+ev.title+'</h3><p>📍 '+(ev.location||'—')+'</p><p>📡 '+ev.source_name+'</p></div>'});G.ev.push(e)}); |
| |
| // Cameras |
| (D.cameras||[]).forEach(cm=>{const e=v.entities.add({position:Cesium.Cartesian3.fromDegrees(cm.lng,cm.lat,50),point:{pixelSize:5,color:Cesium.Color.CYAN,outlineWidth:4,outlineColor:Cesium.Color.fromAlpha(Cesium.Color.CYAN,0.15)},label:{text:'📷 '+cm.country,font:'9px Cairo',fillColor:Cesium.Color.CYAN,pixelOffset:new Cesium.Cartesian2(0,-10),scale:0.7,distanceDisplayCondition:new Cesium.DistanceDisplayCondition(0,1.5e6)},show:L.cm});G.cm.push(e)}); |
| |
| // Panel |
| function rp(tab){ |
| const b=document.getElementById('pb'); |
| if(tab==='ev'){ |
| const evts=D.conflicts||[]; |
| if(!evts.length){b.innerHTML='<div style="text-align:center;padding:20px;color:#64748b">لا توجد أحداث</div>';return} |
| const bgs={explosion:'br',battle:'bo',protest:'bc',strategic:'br',violence:'br'}; |
| const nms={explosion:'غارة/انفجار',battle:'معركة',protest:'احتجاج',strategic:'استراتيجي',violence:'عنف'}; |
| b.innerHTML=evts.slice(0,50).map(e=>'<div class="cd" onclick="fly('+e.lng+','+e.lat+')"><div style="display:flex;justify-content:space-between;margin-bottom:2px"><span class="bg '+(bgs[e.event_type]||'br')+'">'+(nms[e.event_type]||e.event_type)+'</span><span style="font-size:8px;color:#64748b">'+(e.date||'')+'</span></div><div class="t">'+e.title+'</div><div class="m"><span>📍 '+(e.location||'—')+'</span><span>📡 '+e.source_name+'</span></div></div>').join('')} |
| else if(tab==='fl'){ |
| const mil=D.aircraft.military||[]; |
| b.innerHTML='<div style="text-align:center;padding:4px;font-size:10px;color:#0ea5e9;background:rgba(14,165,233,0.06);border-radius:4px;margin-bottom:4px">✈️ '+mil.length+' عسكري | '+(D.aircraft.total||0)+' إجمالي</div>'+mil.slice(0,40).map(a=>'<div class="cd" onclick="fly('+a.lng+','+a.lat+',4e5)"><div style="display:flex;justify-content:space-between"><span style="color:#ef4444;font-weight:800;font-family:JetBrains Mono;direction:ltr;font-size:12px">'+a.callsign+'</span><span class="bg br">عسكري</span></div><div class="m"><span>📏 '+Math.round(a.alt)+'m</span><span>⚡ '+Math.round(a.velocity)+'m/s</span><span>🧭 '+Math.round(a.heading)+'°</span></div></div>').join('')} |
| else if(tab==='nw'){ |
| b.innerHTML=(D.news||[]).slice(0,50).map(n=>'<div class="cd" onclick="window.open(\''+n.link+'\',\'_blank\')">'+(n.is_military?'<span class="bg br" style="margin-bottom:2px;display:inline-block">عسكري</span>':'')+' <div class="t">'+n.title+'</div><div class="m"><span>📡 '+n.source+'</span></div>'+(n.summary?'<div style="font-size:9px;color:#64748b;margin-top:2px">'+n.summary.substring(0,80)+'</div>':'')+'</div>').join('')} |
| else if(tab==='sr'){ |
| const srcs=[{n:'OpenSky',t:'✈️',c:D.aircraft.total||0},{n:'DigiTraffic',t:'🚢',c:(D.ships||[]).length},{n:'GDELT',t:'⚔️',c:(D.conflicts||[]).length},{n:'RSS (13)',t:'📰',c:(D.news||[]).length},{n:'Insecam',t:'📷',c:(D.cameras||[]).length},{n:'Cesium Ion',t:'🌍',c:'—'}]; |
| b.innerHTML=srcs.map(s=>'<div class="cd"><div style="display:flex;justify-content:space-between"><span style="font-weight:700">'+s.n+'</span><span class="bg '+(s.c>0||s.c==='—'?'bg2':'br')+'">'+(s.c>0||s.c==='—'?'✅':'⚠️')+'</span></div><div class="m"><span>'+s.t+'</span><span>📊 '+s.c+'</span></div></div>').join('')} |
| } |
| |
| const ma=(D.aircraft.military||[]).length,ca=(D.aircraft.civil||[]).length; |
| document.getElementById('sA').textContent=(ma+ca)+' ('+ma+' mil)'; |
| document.getElementById('sS').textContent=(D.ships||[]).length; |
| document.getElementById('sC').textContent=(D.conflicts||[]).length; |
| document.getElementById('sN').textContent=(D.news||[]).length; |
| document.getElementById('upd').textContent='🕐 '+(D.meta?.update_time||'—'); |
| const mn=(D.news||[]).filter(n=>n.is_military).slice(0,6); |
| if(mn.length) document.getElementById('tkr').textContent=mn.map(n=>'🔴 '+n.title).join(' ◆ '); |
| setInterval(()=>{document.getElementById('clk').textContent=new Date().toISOString().replace('T',' ').substring(0,19)+' UTC'},1000); |
| rp('ev'); |
| </script></body></html>'''.replace('TOKEN_PLACEHOLDER', CESIUM_TOKEN).replace('DATA_PLACEHOLDER', data_json) |
|
|
|
|
| |
| def get_globe(): |
| data_json = dm.to_json() |
| html = make_globe_html(data_json) |
| b64 = base64.b64encode(html.encode('utf-8')).decode('utf-8') |
| return f'<iframe src="data:text/html;base64,{b64}" style="width:100%;height:82vh;border:none;border-radius:10px;"></iframe>' |
|
|
|
|
| def build_stats(): |
| ss = dm.status |
| now = datetime.now().strftime("%H:%M:%S") |
| src_list = [ |
| ("OpenSky Network", "✈️", ss.get("OpenSky",{}).get("c",0), ss.get("OpenSky",{}).get("s","offline")), |
| ("AIS/DigiTraffic", "🚢", ss.get("Ships",{}).get("c",0), ss.get("Ships",{}).get("s","offline")), |
| ("GDELT", "⚔️", ss.get("GDELT",{}).get("c",0), ss.get("GDELT",{}).get("s","offline")), |
| ("RSS (13 مصدر)", "📰", ss.get("RSS",{}).get("c",0), ss.get("RSS",{}).get("s","offline")), |
| ("Insecam", "📷", ss.get("Cameras",{}).get("c",0), ss.get("Cameras",{}).get("s","offline")), |
| ] |
| items = "" |
| for name, icon, count, status in src_list: |
| color = "#22c55e" if status == "online" else "#ef4444" |
| label = "متصل ✅" if status == "online" else "خطأ ❌" |
| items += f'''<div style="flex:1;min-width:100px;background:linear-gradient(135deg,#1a2332,#1f2937); |
| border:1px solid #2a3a4e;border-radius:8px;padding:10px;text-align:center;"> |
| <div style="font-size:22px;font-weight:900;color:{color}">{count}</div> |
| <div style="font-size:10px;color:#94a3b8">{icon} {name}</div> |
| <div style="font-size:9px;color:{color};margin-top:2px">{label}</div></div>''' |
| return f'<div style="display:flex;gap:6px;flex-wrap:wrap;direction:rtl">{items}</div><div style="text-align:center;font-size:10px;color:#64748b;margin-top:4px">🕐 {now}</div>' |
|
|
|
|
| def build_log(): |
| return "\n".join(list(dm.logs)[-30:]) or "⏳ لم يبدأ بعد" |
|
|
|
|
| def manual_refresh(): |
| dm.fetch_all() |
| return get_globe(), build_stats(), build_log() |
|
|
|
|
| CSS = """ |
| @import url('https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700;800&display=swap'); |
| * {font-family:'Cairo',sans-serif!important} |
| .gradio-container {max-width:100%!important;background:#0a0e17!important;color:#e2e8f0!important;padding:8px!important} |
| .dark {background:#0a0e17!important} |
| .tab-nav button {background:#1a2332!important;color:#94a3b8!important;border:1px solid #2a3a4e!important;border-radius:8px!important;font-weight:700!important} |
| .tab-nav button.selected {background:rgba(14,165,233,0.15)!important;color:#0ea5e9!important;border-color:#0ea5e9!important} |
| .block {background:transparent!important;border:none!important} |
| label {color:#94a3b8!important} |
| textarea {background:#0f172a!important;color:#94a3b8!important;border:1px solid #2a3a4e!important;border-radius:8px!important;font-size:11px!important} |
| button.primary {background:linear-gradient(135deg,#0ea5e9,#06b6d4)!important;border:none!important;font-weight:700!important;border-radius:8px!important;color:white!important} |
| footer {display:none!important} |
| """ |
|
|
| with gr.Blocks(css=CSS, title="🛰️ نظام المراقبة الفضائية", theme=gr.themes.Base(primary_hue="sky", neutral_hue="slate")) as app: |
|
|
| gr.HTML("""<div style="background:linear-gradient(135deg,#0f172a,#1e293b);border:1px solid #2a3a4e;border-radius:10px; |
| padding:12px 20px;display:flex;justify-content:space-between;align-items:center;direction:rtl;flex-wrap:wrap;gap:8px"> |
| <div style="display:flex;align-items:center;gap:10px"> |
| <div style="width:40px;height:40px;border-radius:10px;background:linear-gradient(135deg,#0ea5e9,#06b6d4); |
| display:flex;align-items:center;justify-content:center;font-size:20px">🛰️</div> |
| <div><h1 style="margin:0;font-size:18px;font-weight:900;background:linear-gradient(135deg,#e2e8f0,#0ea5e9); |
| -webkit-background-clip:text;-webkit-text-fill-color:transparent">مركز رصد النزاعات — محاكي الأقمار الاصطناعية</h1> |
| <p style="margin:0;font-size:10px;color:#64748b">CesiumJS 3D · OpenSky Network · AISStream · GDELT · 16 RSS · Insecam — Real APIs</p></div></div> |
| <div style="display:flex;gap:8px"> |
| <div style="display:flex;align-items:center;gap:5px;background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.3); |
| padding:4px 12px;border-radius:16px;font-size:10px;color:#ef4444;font-weight:700"> |
| <span style="width:6px;height:6px;border-radius:50%;background:#ef4444;animation:p 1.5s infinite;display:inline-block"></span>LIVE</div> |
| <div style="display:flex;align-items:center;gap:5px;background:rgba(34,197,94,0.1);border:1px solid rgba(34,197,94,0.3); |
| padding:4px 12px;border-radius:16px;font-size:10px;color:#22c55e;font-weight:700"> |
| <span style="width:6px;height:6px;border-radius:50%;background:#22c55e;animation:p 1.5s infinite;display:inline-block"></span>Real Data</div></div></div> |
| <style>@keyframes p{0%,100%{opacity:1}50%{opacity:0.3}}</style>""") |
|
|
| stats_display = gr.HTML(value=build_stats()) |
| refresh_btn = gr.Button("📡 تحديث من المصادر", variant="primary", size="lg") |
|
|
| with gr.Tabs(): |
| with gr.Tab("🌍 الكرة الأرضية ثلاثية الأبعاد"): |
| globe_display = gr.HTML(value=get_globe()) |
| with gr.Tab("📋 سجل التحديث"): |
| log_display = gr.Textbox(value=build_log(), label="سجل جمع البيانات", lines=25, interactive=False) |
|
|
| refresh_btn.click(fn=manual_refresh, outputs=[globe_display, stats_display, log_display]) |
|
|
| if __name__ == "__main__": |
| app.launch(server_name="0.0.0.0", server_port=7860, show_error=True) |
|
|