313 / app.py
ali3133's picture
Upload 3 files
a4f6aa2 verified
"""
🛰️ 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()
# ═══ Initial fetch at startup ═══
log.info("🚀 Starting initial data fetch...")
dm.fetch_all()
log.info("🚀 Initial fetch complete!")
# ═══ Background auto-refresh ═══
def bg_fetch():
while True:
time.sleep(300) # Wait 5 min before next
try: dm.fetch_all()
except Exception as e: log.error(f"BG: {e}")
threading.Thread(target=bg_fetch, daemon=True).start()
# ═══ Globe HTML ═══
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)
# ═══ Globe as base64 iframe (GUARANTEED to work) ═══
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)