--:--:-- UTC
LIVE
Noticias
Aggregating...
OSINT
Loading OSINT...
Conflicts 17
EVENT TYPE
EVENTS
CONFLICTS
HIDDEN 0
Satellite Imagery — Conflict Zones
Est. Deaths Today
Active Zones
Escalating
Estimates based on ACLED, UCDP & ICG methodologies · Daily variation model-generated
SOURCE: YAHOO FINANCE · FUTURES · AUTO-REFRESH 60s
Commodities — Energy & Metals
AssetTypePriceChange%
Equity Indices
IndexPriceChange%
Crypto
AssetPriceChange%
FX — Major Pairs
PairRateChange%
Telegram OSINT Monitor
Scanning channels...
RGN
TYPE
NOTAM
BREAKING
Aggregating intelligence feeds...
Settings
Display
Light mode
Last Updated
About
CONFLICT·TRACKER
7 Intelligence Modules
Built on infinite-monitor widgets
Static · GitHub Pages deployable
Admin
RESTRICTED ACCESS
// ── MAP EVENTS (from infinite-monitor world-conflicts template) ── const EVENT_TYPES = { "Airstrike": "#ef4444", "Shelling": "#f97316", "Missile Strike": "#a855f7", "Drone Strike": "#06b6d4", "Armed Clash": "#eab308", "Bombing": "#f43f5e", "Protest": "#3b82f6", "Civilian Casualty": "#64748b", }; const EVENT_CONFLICTS = { "Russia-Ukraine": "#facc15", "Gaza": "#f87171", "Iran": "#c084fc", "Lebanon-Israel": "#fb923c", "Sudan": "#4ade80", "Yemen": "#38bdf8", "Myanmar": "#f472b6", "Syria": "#a78bfa", "Somalia": "#2dd4bf", "DRC": "#fbbf24", "Sahel": "#818cf8", "Ethiopia": "#34d399", "Pakistan": "#e879f9", "Haiti": "#fb7185", "Colombia": "#22d3ee", }; const MAP_EVENTS = [ // ── Russia-Ukraine ── { lat: 48.62, lng: 37.80, type: "Armed Clash", conflict: "Russia-Ukraine", location: "Pokrovsk, Donetsk", date: "2026-03-10", description: "Heavy fighting as Russian forces push toward Pokrovsk", casualties: "Unknown" }, { lat: 48.74, lng: 37.56, type: "Shelling", conflict: "Russia-Ukraine", location: "Kostiantynivka, Donetsk", date: "2026-03-09", description: "Russian artillery shelling hit residential area", casualties: "7 killed, 12 wounded" }, { lat: 48.57, lng: 37.17, type: "Airstrike", conflict: "Russia-Ukraine", location: "Kramatorsk, Donetsk", date: "2026-03-08", description: "Glide bomb struck apartment building", casualties: "4 killed" }, { lat: 49.99, lng: 36.23, type: "Missile Strike", conflict: "Russia-Ukraine", location: "Kharkiv", date: "2026-03-11", description: "S-300 missile struck infrastructure", casualties: "3 killed, 15 wounded" }, { lat: 50.45, lng: 30.52, type: "Drone Strike", conflict: "Russia-Ukraine", location: "Kyiv", date: "2026-03-12", description: "Shahed drone wave intercepted over Kyiv, debris caused fires", casualties: "1 killed" }, { lat: 46.97, lng: 31.99, type: "Missile Strike", conflict: "Russia-Ukraine", location: "Mykolaiv", date: "2026-03-07", description: "Kh-59 missile hit port infrastructure", casualties: "2 killed" }, { lat: 46.48, lng: 30.73, type: "Missile Strike", conflict: "Russia-Ukraine", location: "Odesa", date: "2026-03-06", description: "Kalibr cruise missiles struck grain terminal", casualties: "5 killed, 20 wounded" }, { lat: 47.85, lng: 35.12, type: "Airstrike", conflict: "Russia-Ukraine", location: "Zaporizhzhia", date: "2026-03-05", description: "FAB-500 glide bombs hit city center", casualties: "8 killed" }, { lat: 48.45, lng: 34.98, type: "Drone Strike", conflict: "Russia-Ukraine", location: "Dnipro", date: "2026-03-04", description: "Iranian-made drone struck residential tower", casualties: "3 killed" }, { lat: 49.44, lng: 32.06, type: "Missile Strike", conflict: "Russia-Ukraine", location: "Cherkasy", date: "2026-03-03", description: "Ballistic missile targeted energy infrastructure", casualties: "Unknown" }, { lat: 48.30, lng: 38.08, type: "Armed Clash", conflict: "Russia-Ukraine", location: "Avdiivka area, Donetsk", date: "2026-03-10", description: "Positional battles west of Avdiivka ruins", casualties: "Unknown" }, { lat: 49.59, lng: 34.55, type: "Drone Strike", conflict: "Russia-Ukraine", location: "Poltava", date: "2026-03-02", description: "Shahed drones struck military facility", casualties: "11 killed" }, { lat: 50.00, lng: 36.34, type: "Shelling", conflict: "Russia-Ukraine", location: "Vovchansk, Kharkiv", date: "2026-03-08", description: "Intense shelling on northern front positions", casualties: "Unknown" }, { lat: 51.75, lng: 36.19, type: "Drone Strike", conflict: "Russia-Ukraine", location: "Kursk, Russia", date: "2026-03-09", description: "Ukrainian drone strikes on Russian military positions in Kursk", casualties: "Unknown" }, { lat: 54.20, lng: 37.62, type: "Drone Strike", conflict: "Russia-Ukraine", location: "Tula, Russia", date: "2026-03-07", description: "Long-range Ukrainian drone targeted oil depot", casualties: "Unknown" }, // ── Gaza ── { lat: 31.54, lng: 34.50, type: "Airstrike", conflict: "Gaza", location: "Gaza City", date: "2026-03-12", description: "Israeli airstrike hit residential block in Gaza City", casualties: "15 killed" }, { lat: 31.40, lng: 34.35, type: "Airstrike", conflict: "Gaza", location: "Khan Younis", date: "2026-03-11", description: "IDF struck what it said was a command center", casualties: "22 killed" }, { lat: 31.51, lng: 34.46, type: "Bombing", conflict: "Gaza", location: "Jabalia refugee camp", date: "2026-03-10", description: "Heavy bombardment of Jabalia camp", casualties: "30+ killed" }, { lat: 31.30, lng: 34.24, type: "Airstrike", conflict: "Gaza", location: "Rafah", date: "2026-03-09", description: "Airstrikes near Egyptian border", casualties: "12 killed" }, { lat: 31.45, lng: 34.39, type: "Armed Clash", conflict: "Gaza", location: "Nuseirat camp", date: "2026-03-08", description: "Ground operation in central Gaza refugee camp", casualties: "Unknown" }, { lat: 31.35, lng: 34.31, type: "Airstrike", conflict: "Gaza", location: "Deir al-Balah", date: "2026-03-07", description: "Strike hit UN school sheltering displaced families", casualties: "18 killed" }, { lat: 31.53, lng: 34.49, type: "Shelling", conflict: "Gaza", location: "Beit Hanoun", date: "2026-03-06", description: "Tank shelling in northern Gaza", casualties: "9 killed" }, // ── Iran strikes ── { lat: 35.70, lng: 51.42, type: "Airstrike", conflict: "Iran", location: "Tehran, Iran", date: "2026-02-28", description: "US-Israeli strikes on IRGC headquarters in Tehran", casualties: "Unknown" }, { lat: 32.65, lng: 51.68, type: "Airstrike", conflict: "Iran", location: "Isfahan, Iran", date: "2026-02-28", description: "Strikes on nuclear research facilities near Isfahan", casualties: "Unknown" }, { lat: 28.97, lng: 50.84, type: "Airstrike", conflict: "Iran", location: "Bushehr, Iran", date: "2026-02-28", description: "Strikes near Bushehr nuclear power plant", casualties: "Unknown" }, { lat: 27.18, lng: 56.27, type: "Airstrike", conflict: "Iran", location: "Bandar Abbas, Iran", date: "2026-02-28", description: "Strikes on naval base at Bandar Abbas", casualties: "Unknown" }, { lat: 32.05, lng: 44.38, type: "Missile Strike", conflict: "Iran", location: "Baghdad Green Zone, Iraq", date: "2026-03-02", description: "Iranian ballistic missile struck near US embassy", casualties: "3 killed" }, { lat: 36.19, lng: 44.01, type: "Missile Strike", conflict: "Iran", location: "Erbil, Iraq", date: "2026-03-02", description: "IRGC missiles targeted US base near Erbil", casualties: "2 killed" }, { lat: 32.08, lng: 34.78, type: "Missile Strike", conflict: "Iran", location: "Tel Aviv, Israel", date: "2026-03-01", description: "Iranian Fattah-2 missiles intercepted over Tel Aviv", casualties: "Unknown" }, { lat: 26.23, lng: 50.58, type: "Missile Strike", conflict: "Iran", location: "Manama, Bahrain", date: "2026-03-03", description: "Missiles targeted US 5th Fleet HQ", casualties: "Unknown" }, { lat: 36.32, lng: 59.57, type: "Airstrike", conflict: "Iran", location: "Mashhad, Iran", date: "2026-03-01", description: "Strikes on military airfield outside Mashhad", casualties: "Unknown" }, { lat: 34.33, lng: 47.07, type: "Airstrike", conflict: "Iran", location: "Kermanshah, Iran", date: "2026-02-28", description: "Strikes on IRGC missile storage facility", casualties: "Unknown" }, // ── Lebanon-Israel ── { lat: 33.86, lng: 35.51, type: "Airstrike", conflict: "Lebanon-Israel", location: "Dahiyeh, Beirut", date: "2026-03-10", description: "Israeli strikes on Hezbollah stronghold in southern Beirut", casualties: "14 killed" }, { lat: 33.27, lng: 35.20, type: "Armed Clash", conflict: "Lebanon-Israel", location: "Southern Lebanon", date: "2026-03-09", description: "Exchange of fire across Blue Line", casualties: "Unknown" }, { lat: 33.06, lng: 35.10, type: "Airstrike", conflict: "Lebanon-Israel", location: "Bint Jbeil, Lebanon", date: "2026-03-08", description: "Israeli jets struck weapon depot", casualties: "5 killed" }, { lat: 33.37, lng: 35.49, type: "Shelling", conflict: "Lebanon-Israel", location: "Nabatieh, Lebanon", date: "2026-03-07", description: "Artillery exchanges across border", casualties: "3 killed" }, // ── Sudan ── { lat: 15.60, lng: 32.53, type: "Airstrike", conflict: "Sudan", location: "Khartoum, Sudan", date: "2026-03-11", description: "SAF airstrikes on RSF positions in Khartoum", casualties: "Unknown" }, { lat: 13.63, lng: 25.35, type: "Armed Clash", conflict: "Sudan", location: "El Fasher, Darfur", date: "2026-03-10", description: "RSF siege of El Fasher continues with heavy fighting", casualties: "23 killed" }, { lat: 13.18, lng: 30.22, type: "Drone Strike", conflict: "Sudan", location: "El-Obeid, Kordofan", date: "2026-03-09", description: "RSF drone strike on market area", casualties: "12 killed" }, { lat: 15.51, lng: 32.56, type: "Armed Clash", conflict: "Sudan", location: "Omdurman, Sudan", date: "2026-03-08", description: "SAF and RSF clashes in Omdurman", casualties: "Unknown" }, { lat: 19.17, lng: 30.48, type: "Armed Clash", conflict: "Sudan", location: "Wadi Halfa, Sudan", date: "2026-03-06", description: "Fighting near Egyptian border crossing", casualties: "Unknown" }, { lat: 11.75, lng: 29.70, type: "Bombing", conflict: "Sudan", location: "Kadugli, Nuba Mts", date: "2026-03-05", description: "Aerial bombardment of civilian areas", casualties: "9 killed" }, { lat: 14.00, lng: 33.52, type: "Airstrike", conflict: "Sudan", location: "Wad Madani, Sudan", date: "2026-03-04", description: "SAF airstrike on RSF-held city", casualties: "Unknown" }, { lat: 12.05, lng: 24.89, type: "Armed Clash", conflict: "Sudan", location: "Nyala, Darfur", date: "2026-03-03", description: "RSF assault on residential neighborhoods", casualties: "35 killed" }, // ── Yemen ── { lat: 15.37, lng: 44.19, type: "Airstrike", conflict: "Yemen", location: "Sana'a, Yemen", date: "2026-03-11", description: "US strikes on Houthi military targets", casualties: "Unknown" }, { lat: 14.80, lng: 42.95, type: "Airstrike", conflict: "Yemen", location: "Hodeidah, Yemen", date: "2026-03-10", description: "Strikes on Houthi port and weapons depots", casualties: "Unknown" }, { lat: 13.58, lng: 43.25, type: "Missile Strike", conflict: "Yemen", location: "Bab el-Mandeb Strait", date: "2026-03-09", description: "Houthi anti-ship missile attacked commercial vessel", casualties: "Unknown" }, { lat: 12.78, lng: 45.02, type: "Drone Strike", conflict: "Yemen", location: "Gulf of Aden", date: "2026-03-08", description: "US Navy shot down Houthi drones near shipping lane", casualties: "Unknown" }, { lat: 15.48, lng: 44.22, type: "Drone Strike", conflict: "Yemen", location: "Sana'a airport area", date: "2026-03-06", description: "Drone strike on Houthi drone storage facility", casualties: "Unknown" }, // ── Myanmar ── { lat: 21.97, lng: 96.08, type: "Airstrike", conflict: "Myanmar", location: "Mandalay, Myanmar", date: "2026-03-08", description: "Junta airstrikes on resistance positions", casualties: "Unknown" }, { lat: 16.87, lng: 96.20, type: "Protest", conflict: "Myanmar", location: "Yangon, Myanmar", date: "2026-03-07", description: "Anti-junta protests met with crackdown", casualties: "2 killed" }, { lat: 22.10, lng: 97.04, type: "Armed Clash", conflict: "Myanmar", location: "Shan State, Myanmar", date: "2026-03-06", description: "TNLA clashes with junta forces in northern Shan", casualties: "Unknown" }, { lat: 25.38, lng: 97.39, type: "Armed Clash", conflict: "Myanmar", location: "Kachin State, Myanmar", date: "2026-03-05", description: "KIA advances against military positions", casualties: "Unknown" }, { lat: 19.75, lng: 96.13, type: "Airstrike", conflict: "Myanmar", location: "Naypyidaw area, Myanmar", date: "2026-03-04", description: "Junta strikes on Karen resistance near capital", casualties: "8 killed" }, // ── Syria ── { lat: 36.20, lng: 37.16, type: "Airstrike", conflict: "Syria", location: "Aleppo, Syria", date: "2026-03-10", description: "Russian airstrikes on opposition areas", casualties: "6 killed" }, { lat: 35.00, lng: 36.75, type: "Armed Clash", conflict: "Syria", location: "Homs, Syria", date: "2026-03-08", description: "Clashes between HTS and regime remnants", casualties: "Unknown" }, { lat: 34.80, lng: 40.00, type: "Airstrike", conflict: "Syria", location: "Deir ez-Zor, Syria", date: "2026-03-07", description: "US strikes on ISIS cells", casualties: "Unknown" }, { lat: 36.35, lng: 41.22, type: "Armed Clash", conflict: "Syria", location: "Hasakah, Syria", date: "2026-03-06", description: "SDF-Turkish proxy clashes in northeast", casualties: "Unknown" }, // ── Somalia ── { lat: 2.05, lng: 45.32, type: "Airstrike", conflict: "Somalia", location: "Mogadishu, Somalia", date: "2026-03-09", description: "US airstrike on al-Shabaab targets", casualties: "Unknown" }, { lat: 3.51, lng: 42.54, type: "Armed Clash", conflict: "Somalia", location: "Gedo, Somalia", date: "2026-03-08", description: "Somali forces battled al-Shabaab", casualties: "15 killed" }, { lat: 2.47, lng: 45.00, type: "Bombing", conflict: "Somalia", location: "Beledweyne, Somalia", date: "2026-03-06", description: "Al-Shabaab car bomb at checkpoint", casualties: "8 killed" }, { lat: 9.56, lng: 44.06, type: "Armed Clash", conflict: "Somalia", location: "Hargeisa, Somaliland", date: "2026-03-05", description: "Tensions between Somaliland and Puntland forces", casualties: "Unknown" }, // ── DRC ── { lat: -1.68, lng: 29.22, type: "Armed Clash", conflict: "DRC", location: "Goma, DRC", date: "2026-03-11", description: "M23 rebels advance on Goma with Rwandan support", casualties: "Unknown" }, { lat: 0.49, lng: 29.47, type: "Armed Clash", conflict: "DRC", location: "Beni, DRC", date: "2026-03-09", description: "ADF militia attack on village near Beni", casualties: "20 killed" }, { lat: 1.56, lng: 30.25, type: "Civilian Casualty", conflict: "DRC", location: "Bunia, Ituri", date: "2026-03-07", description: "CODECO militia massacred civilians", casualties: "30+ killed" }, { lat: -2.50, lng: 28.86, type: "Armed Clash", conflict: "DRC", location: "Bukavu, South Kivu", date: "2026-03-05", description: "Heavy fighting between FARDC and M23", casualties: "Unknown" }, // ── Sahel ── { lat: 12.65, lng: -8.00, type: "Armed Clash", conflict: "Sahel", location: "Bamako area, Mali", date: "2026-03-08", description: "JNIM attack on Malian army outpost", casualties: "11 killed" }, { lat: 14.50, lng: -1.50, type: "Bombing", conflict: "Sahel", location: "Djibo, Burkina Faso", date: "2026-03-07", description: "ISWAP bomb attack on aid convoy", casualties: "7 killed" }, { lat: 13.51, lng: 2.11, type: "Armed Clash", conflict: "Sahel", location: "Niamey area, Niger", date: "2026-03-06", description: "Jihadist ambush on Niger military patrol", casualties: "5 killed" }, { lat: 16.27, lng: -0.05, type: "Armed Clash", conflict: "Sahel", location: "Timbuktu, Mali", date: "2026-03-05", description: "Tuareg separatists clashed with Wagner-backed forces", casualties: "Unknown" }, // ── Ethiopia ── { lat: 13.50, lng: 39.47, type: "Armed Clash", conflict: "Ethiopia", location: "Mekelle, Tigray", date: "2026-03-07", description: "Sporadic violence despite ceasefire", casualties: "Unknown" }, { lat: 7.68, lng: 36.83, type: "Armed Clash", conflict: "Ethiopia", location: "Jimma, Oromia", date: "2026-03-06", description: "OLA clashes with Ethiopian military", casualties: "Unknown" }, { lat: 11.59, lng: 37.39, type: "Civilian Casualty", conflict: "Ethiopia", location: "Bahir Dar, Amhara", date: "2026-03-05", description: "Fano militia-related violence", casualties: "14 killed" }, // ── Pakistan ── { lat: 30.20, lng: 67.00, type: "Bombing", conflict: "Pakistan", location: "Quetta, Balochistan", date: "2026-03-08", description: "BLA bomb attack on security checkpoint", casualties: "6 killed" }, { lat: 34.01, lng: 71.58, type: "Bombing", conflict: "Pakistan", location: "Peshawar, KPK", date: "2026-03-06", description: "TTP suicide bombing near police station", casualties: "4 killed" }, { lat: 32.37, lng: 69.96, type: "Armed Clash", conflict: "Pakistan", location: "South Waziristan", date: "2026-03-04", description: "Military operation against TTP militants", casualties: "Unknown" }, // ── Haiti ── { lat: 18.54, lng: -72.34, type: "Armed Clash", conflict: "Haiti", location: "Port-au-Prince, Haiti", date: "2026-03-10", description: "Gang warfare in capital continues", casualties: "12 killed" }, { lat: 18.47, lng: -72.31, type: "Civilian Casualty", conflict: "Haiti", location: "Cité Soleil, Haiti", date: "2026-03-08", description: "Civilians caught in crossfire between gangs", casualties: "8 killed" }, // ── Colombia ── { lat: 7.12, lng: -73.12, type: "Armed Clash", conflict: "Colombia", location: "Bucaramanga area, Colombia", date: "2026-03-07", description: "ELN ambush on Colombian military convoy", casualties: "3 killed" }, { lat: 1.80, lng: -78.76, type: "Bombing", conflict: "Colombia", location: "Tumaco, Colombia", date: "2026-03-05", description: "Car bomb linked to FARC dissidents", casualties: "2 killed" }, { lat: 2.44, lng: -76.61, type: "Armed Clash", conflict: "Colombia", location: "Cauca, Colombia", date: "2026-03-03", description: "Clashes between armed groups in Cauca", casualties: "5 killed" }, ]; const MAP_REGIONS = [ { label:"🌍 Global", center:[20,15], zoom:2 }, { label:"🇺🇦 Ukraine", center:[35,49], zoom:5.5 }, { label:"🇵🇸 Gaza", center:[34.4,31.4], zoom:9 }, { label:"🇮🇷 Iran", center:[53,33], zoom:5 }, { label:"🇱🇧 Lebanon", center:[35.5,33.5], zoom:8 }, { label:"🇸🇩 Sudan", center:[30,15], zoom:5 }, { label:"🇾🇪 Yemen", center:[44,15], zoom:6 }, { label:"🇲🇲 Myanmar", center:[96,20], zoom:5.5 }, { label:"🌍 Sahel", center:[-2,14], zoom:5 }, { label:"🇨🇩 DRC", center:[29,-1], zoom:6 }, ]; // ── SATELLITE ZONES ─────────────────────────────────────── const SAT_ZONES = [ // ── CRITICAL ── { id: "donetsk", name: "Donetsk Frontline", region: "Ukraine", conflict: "Russia-Ukraine War", lat: 48.02, lon: 37.80, zoom: 10, status: "critical", description: "Heavy fighting along Avdiivka-Pokrovsk highway. Russian forces pushing westward with intense artillery exchanges.", lastEvent: "2026-03-12", color: "#fbbf24", }, { id: "gaza", name: "Gaza Strip", region: "Palestine", conflict: "Gaza Conflict", lat: 31.45, lon: 34.40, zoom: 11, status: "critical", description: "Ongoing Israeli bombardment across northern and central Gaza. Massive civilian infrastructure destruction.", lastEvent: "2026-03-12", color: "#ef4444", }, { id: "khartoum", name: "Khartoum", region: "Sudan", conflict: "Sudan Civil War", lat: 15.59, lon: 32.53, zoom: 10, status: "critical", description: "RSF and SAF artillery exchanges in the capital. Urban warfare destroying residential neighborhoods.", lastEvent: "2026-03-12", color: "#a78bfa", }, { id: "lebanon", name: "Southern Lebanon", region: "Lebanon", conflict: "Israel-Hezbollah", lat: 33.27, lon: 35.50, zoom: 11, status: "critical", description: "Cross-border exchanges between IDF and Hezbollah. Israeli airstrikes on Dahieh suburbs and Bekaa Valley.", lastEvent: "2026-03-12", color: "#f43f5e", }, // ── INTENSE ── { id: "iran", name: "Tehran / Isfahan", region: "Iran", conflict: "Iran-Israel Tensions", lat: 32.65, lon: 51.68, zoom: 9, status: "intense", description: "Heightened military activity around nuclear sites. Satellite imagery shows IRGC mobilizations and air defense redeployments.", lastEvent: "2026-03-11", color: "#f59e0b", }, { id: "sagaing", name: "Sagaing Region", region: "Myanmar", conflict: "Myanmar Civil War", lat: 22.10, lon: 95.00, zoom: 10, status: "intense", description: "Junta airstrikes on resistance-held towns. Resistance forces capturing military outposts across Shan State.", lastEvent: "2026-03-11", color: "#34d399", }, { id: "goma", name: "Goma, North Kivu", region: "DR Congo", conflict: "DRC-M23 Conflict", lat: -1.68, lon: 29.22, zoom: 11, status: "intense", description: "M23 rebels advancing near Goma with Rwandan backing. Government forces shelling rebel positions around Sake.", lastEvent: "2026-03-12", color: "#84cc16", }, { id: "west-bank", name: "West Bank", region: "Palestine", conflict: "Israeli Occupation", lat: 32.22, lon: 35.25, zoom: 10, status: "intense", description: "IDF raids in Jenin and Tulkarm refugee camps. Settler violence escalating across the occupied territories.", lastEvent: "2026-03-12", color: "#fb7185", }, // ── ESCALATING ── { id: "sanaa", name: "Sanaa / Hodeidah", region: "Yemen", conflict: "Yemen-Houthi / Red Sea", lat: 15.35, lon: 44.21, zoom: 10, status: "escalating", description: "US-UK airstrikes on Houthi positions. Houthi anti-ship missile launches targeting Red Sea commercial shipping.", lastEvent: "2026-03-11", color: "#e879f9", }, { id: "tigray", name: "Amhara / Tigray", region: "Ethiopia", conflict: "Ethiopia Internal", lat: 12.60, lon: 39.47, zoom: 9, status: "escalating", description: "Fano militia clashes with federal forces in Amhara. Post-ceasefire tensions and disarmament disputes in Tigray.", lastEvent: "2026-03-10", color: "#22d3ee", }, { id: "sahel", name: "Sahel / Mali-Burkina", region: "West Africa", conflict: "Sahel Insurgency", lat: 14.50, lon: -1.50, zoom: 8, status: "escalating", description: "JNIM and ISGS attacks on military and civilian targets. Wagner/Africa Corps supporting junta operations.", lastEvent: "2026-03-10", color: "#c084fc", }, { id: "idlib", name: "Idlib / NW Syria", region: "Syria", conflict: "Syria Conflict", lat: 35.77, lon: 36.73, zoom: 10, status: "escalating", description: "HTS consolidating control in Idlib. Russian airstrikes continue. Kurdish-ISIS clashes in Deir ez-Zor.", lastEvent: "2026-03-10", color: "#fca5a5", }, // ── ACTIVE ── { id: "mogadishu", name: "Mogadishu / Lower Shabelle", region: "Somalia", conflict: "Somalia-Al Shabaab", lat: 2.05, lon: 45.32, zoom: 10, status: "active", description: "Al-Shabaab car bombings and mortar attacks. Somali forces conducting clearing operations in Lower Shabelle.", lastEvent: "2026-03-11", color: "#fb923c", }, { id: "haiti", name: "Port-au-Prince", region: "Haiti", conflict: "Gang Warfare", lat: 18.54, lon: -72.34, zoom: 11, status: "active", description: "Armed gangs controlling 80% of the capital. UN-backed multinational force struggling to restore order.", lastEvent: "2026-03-11", color: "#60a5fa", }, { id: "libya", name: "Tripoli / Sirte", region: "Libya", conflict: "Libyan Civil Division", lat: 31.20, lon: 16.60, zoom: 8, status: "active", description: "Rival government militias skirmishing near oil facilities. Mercenary presence destabilizing southern regions.", lastEvent: "2026-03-09", color: "#fcd34d", }, { id: "borno", name: "Borno State", region: "Nigeria", conflict: "Boko Haram / ISWAP", lat: 11.83, lon: 13.15, zoom: 9, status: "active", description: "ISWAP attacks on military bases near Lake Chad. Boko Haram targeting civilian convoys in northeast Nigeria.", lastEvent: "2026-03-09", color: "#4ade80", }, { id: "balochistan", name: "Balochistan", region: "Pakistan", conflict: "Baloch Insurgency", lat: 29.00, lon: 66.00, zoom: 8, status: "active", description: "BLA attacks on Pakistani security forces and Chinese-linked infrastructure near Gwadar port.", lastEvent: "2026-03-08", color: "#38bdf8", }, { id: "nagorno", name: "Armenia-Azerbaijan Border", region: "South Caucasus", conflict: "Post-Karabakh Tensions", lat: 39.82, lon: 46.75, zoom: 10, status: "active", description: "Border skirmishes continue post-Karabakh. Azerbaijan military buildup near Armenian Syunik corridor.", lastEvent: "2026-03-08", color: "#2dd4bf", }, ]; // ── DEATH TRACKER DATA ──────────────────────────────────── const DEATH_CONFLICTS = [ { id: "sudan", name: "Sudan Civil War", region: "East Africa", country: "Sudan", parties: ["SAF", "RSF"], avgDailyDeaths: 95, cumulativeDeaths: 150000, startYear: 2023, severity: "major_war", trend: "escalating", description: "Civil war between the Sudanese Armed Forces and the Rapid Support Forces, causing massive civilian casualties and displacement." }, { id: "ukraine", name: "Russia-Ukraine War", region: "Eastern Europe", country: "Ukraine", parties: ["Ukraine", "Russia"], avgDailyDeaths: 85, cumulativeDeaths: 200000, startYear: 2022, severity: "major_war", trend: "stable", description: "Full-scale Russian invasion of Ukraine with intense trench warfare and artillery exchanges along the eastern front." }, { id: "gaza", name: "Gaza War", region: "Middle East", country: "Palestine", parties: ["Israel", "Hamas", "PIJ"], avgDailyDeaths: 70, cumulativeDeaths: 55000, startYear: 2023, severity: "major_war", trend: "stable", description: "Israeli military operations in Gaza following the October 7 attacks, resulting in devastating civilian casualties." }, { id: "myanmar", name: "Myanmar Civil War", region: "Southeast Asia", country: "Myanmar", parties: ["Military Junta", "NUG", "Ethnic Armies"], avgDailyDeaths: 40, cumulativeDeaths: 60000, startYear: 2021, severity: "war", trend: "escalating", description: "Nationwide armed resistance against the military junta following the 2021 coup, with multiple ethnic armed organizations involved." }, { id: "ethiopia-amhara", name: "Ethiopia (Amhara)", region: "East Africa", country: "Ethiopia", parties: ["Ethiopian Military", "Fano Militia"], avgDailyDeaths: 18, cumulativeDeaths: 12000, startYear: 2023, severity: "war", trend: "escalating", description: "Armed conflict in the Amhara region between federal forces and Fano militia fighters resisting disarmament." }, { id: "drc", name: "Eastern DRC Conflict", region: "Central Africa", country: "DR Congo", parties: ["DRC Army", "M23", "ADF", "FDLR"], avgDailyDeaths: 30, cumulativeDeaths: 120000, startYear: 2004, severity: "war", trend: "escalating", description: "Ongoing conflict in North Kivu and Ituri provinces involving multiple armed groups, with M23 offensive intensifying." }, { id: "sahel-burkina", name: "Burkina Faso Insurgency", region: "West Africa", country: "Burkina Faso", parties: ["Military Junta", "JNIM", "ISSP"], avgDailyDeaths: 22, cumulativeDeaths: 26000, startYear: 2015, severity: "war", trend: "escalating", description: "Jihadist insurgency across Burkina Faso with the military government struggling to contain armed groups." }, { id: "sahel-mali", name: "Mali Conflict", region: "West Africa", country: "Mali", parties: ["Military Junta", "JNIM", "IS Sahel", "Tuareg Groups"], avgDailyDeaths: 15, cumulativeDeaths: 22000, startYear: 2012, severity: "war", trend: "stable", description: "Multi-sided conflict involving jihadist groups, separatist movements, and the ruling military junta." }, { id: "sahel-niger", name: "Niger Insurgency", region: "West Africa", country: "Niger", parties: ["Military Junta", "JNIM", "IS Sahel"], avgDailyDeaths: 8, cumulativeDeaths: 7000, startYear: 2015, severity: "conflict", trend: "stable", description: "Jihadist violence in western and southeastern border regions of Niger." }, { id: "somalia", name: "Somalia Conflict", region: "East Africa", country: "Somalia", parties: ["Somali Government", "Al-Shabaab"], avgDailyDeaths: 12, cumulativeDeaths: 45000, startYear: 2006, severity: "war", trend: "de-escalating", description: "Long-running insurgency by al-Shabaab against the federal government, with ongoing military offensives." }, { id: "syria", name: "Syria Conflict", region: "Middle East", country: "Syria", parties: ["HTS", "SDF", "Turkey", "Various Factions"], avgDailyDeaths: 8, cumulativeDeaths: 500000, startYear: 2011, severity: "conflict", trend: "de-escalating", description: "Reduced but ongoing conflict following the fall of the Assad regime, with tensions between rival factions." }, { id: "yemen", name: "Yemen Conflict", region: "Middle East", country: "Yemen", parties: ["Houthis", "Saudi Coalition", "STC"], avgDailyDeaths: 10, cumulativeDeaths: 377000, startYear: 2014, severity: "conflict", trend: "stable", description: "Complex civil war and humanitarian crisis, with Houthi missile and drone attacks on Red Sea shipping." }, { id: "iran", name: "Iran (Internal & Proxy Conflict)", region: "Middle East", country: "Iran", parties: ["IRGC", "PJAK", "Jaish ul-Adl", "Baloch Militants", "Israel"], avgDailyDeaths: 7, cumulativeDeaths: 15000, startYear: 2003, severity: "conflict", trend: "escalating", description: "Ongoing militant insurgencies in border regions (Kurdistan, Sistan-Baluchestan), internal repression of dissent, and escalating shadow war with Israel including strikes on military infrastructure." }, { id: "mexico", name: "Mexico Drug War", region: "Latin America", country: "Mexico", parties: ["Mexican State", "Sinaloa Cartel", "CJNG", "Various Cartels"], avgDailyDeaths: 80, cumulativeDeaths: 450000, startYear: 2006, severity: "major_war", trend: "stable", description: "Widespread cartel violence and organized crime across Mexico, with daily killings rivaling active war zones." }, { id: "colombia", name: "Colombia Armed Conflict", region: "Latin America", country: "Colombia", parties: ["Colombian State", "ELN", "FARC Dissidents", "Drug Gangs"], avgDailyDeaths: 12, cumulativeDeaths: 260000, startYear: 1964, severity: "conflict", trend: "stable", description: "Ongoing violence from ELN guerrillas, FARC dissidents, and narco-trafficking groups despite peace process." }, { id: "haiti", name: "Haiti Gang Violence", region: "Latin America", country: "Haiti", parties: ["Gangs", "Multinational Force"], avgDailyDeaths: 10, cumulativeDeaths: 8000, startYear: 2021, severity: "conflict", trend: "escalating", description: "Rampant gang violence in Port-au-Prince and beyond, overwhelming the collapsing state." }, { id: "nigeria-ne", name: "Nigeria (Northeast)", region: "West Africa", country: "Nigeria", parties: ["Nigerian Military", "Boko Haram", "ISWAP"], avgDailyDeaths: 10, cumulativeDeaths: 40000, startYear: 2009, severity: "conflict", trend: "de-escalating", description: "Boko Haram and ISWAP insurgency in the Lake Chad basin, with ongoing military operations." }, { id: "pakistan", name: "Pakistan (Balochistan/KP)", region: "South Asia", country: "Pakistan", parties: ["Pakistani Military", "TTP", "BLA"], avgDailyDeaths: 8, cumulativeDeaths: 70000, startYear: 2004, severity: "conflict", trend: "escalating", description: "Resurgent militant violence from TTP in Khyber Pakhtunkhwa and BLA separatists in Balochistan." }, { id: "afghanistan", name: "Afghanistan (IS-KP)", region: "South Asia", country: "Afghanistan", parties: ["Taliban Government", "IS-KP", "NRF"], avgDailyDeaths: 6, cumulativeDeaths: 175000, startYear: 2001, severity: "conflict", trend: "de-escalating", description: "Reduced but continuing violence from IS-Khorasan Province attacks and sporadic resistance activity." }, { id: "lebanon", name: "Lebanon-Israel Border", region: "Middle East", country: "Lebanon", parties: ["Israel", "Hezbollah"], avgDailyDeaths: 5, cumulativeDeaths: 4500, startYear: 2023, severity: "conflict", trend: "de-escalating", description: "Cross-border exchanges between Hezbollah and Israel following the Gaza escalation, with fragile ceasefire." }, { id: "cameroon", name: "Cameroon (Anglophone)", region: "Central Africa", country: "Cameroon", parties: ["Cameroonian Military", "Ambazonia Separatists"], avgDailyDeaths: 4, cumulativeDeaths: 6000, startYear: 2017, severity: "conflict", trend: "stable", description: "Separatist conflict in the English-speaking Northwest and Southwest regions of Cameroon." }, { id: "mozambique", name: "Mozambique (Cabo Delgado)", region: "East Africa", country: "Mozambique", parties: ["Mozambican Military", "ASWJ/IS-Moz", "SADC Force"], avgDailyDeaths: 3, cumulativeDeaths: 5000, startYear: 2017, severity: "conflict", trend: "de-escalating", description: "Islamist insurgency in Cabo Delgado province with regional military support helping contain the violence." }, ]; function _seededRandom(seed) { let hash = 0; for(let i=0;i new Date(now - hrs*3600000).toISOString(); const f = hrs => new Date(now + hrs*3600000).toISOString(); let id = 0; const mk = (o) => ({ id: 'N'+(++id), notamId:o.notamId, type:o.type, severity:o.severity, location:o.location, fir:o.fir, country:o.country, region:o.region, lat:o.lat, lng:o.lng, lowerAlt:o.lowerAlt, upperAlt:o.upperAlt, effectiveFrom:o.effectiveFrom, effectiveTo:o.effectiveTo, issuedAt:o.issuedAt, description:o.description, keywords:o.keywords||[], isConflictZone:['Ukraine','Gaza','Yemen','Sudan','Myanmar','Syria','Iran','Lebanon','Somalia','Iraq','Libya','Afghanistan'].some(z=>o.country.includes(z)||o.location.includes(z)), isNew:(now-new Date(o.issuedAt).getTime())<6*3600000, }); return [ mk({notamId:'A0147/26',type:'CONFLICT',severity:'critical',location:'UKRAINE EASTERN AIRSPACE',fir:'UKDV',country:'Ukraine',region:'Europe',lat:48.38,lng:37.62,lowerAlt:'SFC',upperAlt:'UNL',effectiveFrom:h(720),effectiveTo:f(2160),issuedAt:h(2),description:'AIRSPACE PROHIBITED. ALL AIRCRAFT OPERATIONS PROHIBITED WITHIN EASTERN UKRAINE FIR DUE TO ONGOING ARMED CONFLICT. ANTI-AIRCRAFT SYSTEMS ACTIVE.',keywords:['PROHIBITED','CONFLICT','WAR','DANGER']}), mk({notamId:'A0212/26',type:'CONFLICT',severity:'critical',location:'GAZA STRIP AIRSPACE',fir:'LLLL',country:'Gaza',region:'Middle East',lat:31.42,lng:34.40,lowerAlt:'SFC',upperAlt:'FL600',effectiveFrom:h(4320),effectiveTo:f(4320),issuedAt:h(4),description:'DANGER AREA. GAZA STRIP AND SURROUNDING AIRSPACE PROHIBITED. ACTIVE MILITARY OPERATIONS INCLUDING AIR AND GROUND COMBAT.',keywords:['PROHIBITED','CONFLICT','WAR']}), mk({notamId:'B0089/26',type:'CONFLICT',severity:'critical',location:'YEMEN - HOUTHI CONTROLLED',fir:'OYSC',country:'Yemen',region:'Middle East',lat:15.35,lng:44.20,lowerAlt:'SFC',upperAlt:'UNL',effectiveFrom:h(2160),effectiveTo:f(4320),issuedAt:h(12),description:'AIRSPACE RESTRICTED. YEMEN FIR WESTERN SECTOR PROHIBITED. ANTI-SHIP MISSILE AND DRONE LAUNCHES ONGOING.',keywords:['PROHIBITED','MISSILE','CONFLICT']}), mk({notamId:'C0045/26',type:'CONFLICT',severity:'critical',location:'SUDAN KHARTOUM AREA',fir:'HSSS',country:'Sudan',region:'Africa',lat:15.60,lng:32.53,lowerAlt:'SFC',upperAlt:'FL450',effectiveFrom:h(4320),effectiveTo:f(2160),issuedAt:h(72),description:'AIRSPACE PROHIBITED. KHARTOUM FIR. ARMED CONFLICT BETWEEN SAF AND RSF. ALL CIVIL AVIATION PROHIBITED.',keywords:['PROHIBITED','CONFLICT','WAR']}), mk({notamId:'D0033/26',type:'CONFLICT',severity:'high',location:'MYANMAR CENTRAL AIRSPACE',fir:'VYYY',country:'Myanmar',region:'Asia-Pacific',lat:19.75,lng:96.13,lowerAlt:'SFC',upperAlt:'FL400',effectiveFrom:h(4320),effectiveTo:f(4320),issuedAt:h(168),description:'DANGER AREA. MYANMAR FIR. MILITARY JUNTA AIRSTRIKES ONGOING.',keywords:['DANGER AREA','CONFLICT']}), mk({notamId:'A0301/26',type:'CONFLICT',severity:'critical',location:'SYRIA EASTERN AIRSPACE',fir:'OSTT',country:'Syria',region:'Middle East',lat:35.20,lng:38.90,lowerAlt:'SFC',upperAlt:'UNL',effectiveFrom:h(8760),effectiveTo:f(4320),issuedAt:h(240),description:'PROHIBITED AREA. SYRIA FIR. MULTIPLE MILITARY FORCES ACTIVE.',keywords:['PROHIBITED','CONFLICT']}), mk({notamId:'A0500/26',type:'CONFLICT',severity:'high',location:'IRAN AIRSPACE',fir:'OIIX',country:'Iran',region:'Middle East',lat:32.43,lng:53.69,lowerAlt:'SFC',upperAlt:'UNL',effectiveFrom:h(720),effectiveTo:f(2160),issuedAt:h(36),description:'RESTRICTED AREA. IRAN FIR. HEIGHTENED MILITARY ACTIVITY. IRGC AIR DEFENSE SYSTEMS ON MAXIMUM ALERT.',keywords:['RESTRICTED','CONFLICT']}), mk({notamId:'M0234/26',type:'MIL',severity:'medium',location:'NATO EXERCISE - BALTIC SEA',fir:'EDMM',country:'Germany',region:'Europe',lat:55.50,lng:16.00,lowerAlt:'SFC',upperAlt:'FL450',effectiveFrom:h(24),effectiveTo:f(168),issuedAt:h(3),description:'MILITARY EXERCISE "BALTIC SENTINEL 26". LARGE SCALE NATO AIR AND NAVAL EXERCISE. SIMULATED FIRING AREAS ACTIVE.',keywords:['MIL','EXERCISE','FIRING']}), mk({notamId:'M0302/26',type:'MIL',severity:'medium',location:'CHINESE MILITARY - SOUTH CHINA SEA',fir:'ZGZU',country:'China',region:'Asia-Pacific',lat:16.50,lng:112.50,lowerAlt:'SFC',upperAlt:'UNL',effectiveFrom:h(72),effectiveTo:f(240),issuedAt:h(1),description:'DANGER AREA. PLA NAVY AND AIR FORCE EXERCISE. SOUTH CHINA SEA. LIVE FIRING INCLUDING ANTI-SHIP MISSILES.',keywords:['MIL','EXERCISE','FIRING','MISSILE']}), mk({notamId:'M0303/26',type:'MIL',severity:'high',location:'CHINESE MILITARY - TAIWAN STRAIT',fir:'ZGZU',country:'China',region:'Asia-Pacific',lat:24.00,lng:118.50,lowerAlt:'SFC',upperAlt:'FL600',effectiveFrom:h(12),effectiveTo:f(72),issuedAt:h(2),description:'RESTRICTED AREA. TAIWAN STRAIT. PLA MILITARY EXERCISE. BALLISTIC MISSILE FIRING ZONES ACTIVE.',keywords:['MIL','EXERCISE','MISSILE']}), mk({notamId:'T0891/26',type:'TFR',severity:'medium',location:'WASHINGTON DC AREA',fir:'KZDC',country:'United States',region:'Americas',lat:38.90,lng:-77.04,lowerAlt:'SFC',upperAlt:'FL180',effectiveFrom:h(2),effectiveTo:f(12),issuedAt:h(1),description:'TFR. TEMPORARY FLIGHT RESTRICTION. WASHINGTON DC SPECIAL FLIGHT RULES AREA. VIP MOVEMENT.',keywords:['TFR','VIP']}), mk({notamId:'L0047/26',type:'LAUNCH',severity:'high',location:'NORTH KOREA - MISSILE TEST',fir:'ZKKP',country:'North Korea',region:'Asia-Pacific',lat:39.66,lng:124.71,lowerAlt:'SFC',upperAlt:'UNL',effectiveFrom:h(2),effectiveTo:f(48),issuedAt:h(1),description:'DANGER AREA. DPRK MISSILE LAUNCH NOTIFICATION. BALLISTIC MISSILE TEST EXPECTED. SEA OF JAPAN IMPACT ZONE.',keywords:['MISSILE','LAUNCH','DANGER AREA']}), mk({notamId:'L0045/26',type:'LAUNCH',severity:'medium',location:'BAIKONUR COSMODROME',fir:'UATT',country:'Kazakhstan',region:'Asia-Pacific',lat:45.96,lng:63.31,lowerAlt:'SFC',upperAlt:'UNL',effectiveFrom:f(6),effectiveTo:f(12),issuedAt:h(5),description:'LAUNCH WINDOW. SOYUZ ROCKET LAUNCH FROM BAIKONUR. DEBRIS FALL ZONES ACTIVE.',keywords:['LAUNCH','ROCKET']}), ]; } // ── Utility ─────────────────────────────────────────────── function progSet(pct) { const fill=document.getElementById('prog-fill'), bar=document.getElementById('prog'); if(!fill||!bar) return; bar.classList.remove('idle'); fill.style.width=pct+'%'; if(pct>=100) setTimeout(()=>{bar.classList.add('idle');setTimeout(()=>{bar.classList.remove('idle');fill.style.width='0%';},700);},200); } function colStatus(id,state,text='') { const el=document.getElementById('cs-'+id); if(!el) return; if(state==='loading') el.innerHTML=''; else if(state==='ok') el.innerHTML=''+(text?' '+text+'':''); else if(state==='err') el.innerHTML=''; else el.innerHTML=''; } function sortByDate(arr) { return arr.sort((a,b)=>b.d-a.d); } function merge(existing, incoming) { const ids=new Set(existing.map(x=>x.id)); const nw=incoming.filter(x=>!ids.has(x.id)); return {merged:[...nw,...existing].slice(0,600),newCount:nw.length}; } function timeAgo(ts) { const m=Math.floor((Date.now()-ts)/60000); if(m<1) return 'now'; if(m<60) return m+'m'; if(m<1440) return Math.floor(m/60)+'h'; return Math.floor(m/1440)+'d'; } function markRead(href) { readSet.add(href); localStorage.setItem('ct_read',JSON.stringify([...readSet].slice(-500))); } function openItem(href) { window.open(href,'_blank'); markRead(href); } const KEYWORDS = { conflict:['attack','airstrike','missile','killed','explosion','bomb','strike','offensive','combat'], ukraine:['ukraine','zelensky','kyiv','kharkiv','donbas','zaporizhzhia','russia'], mideast:['gaza','hamas','israel','rafah','hezbollah','iran','tehran'], africa:['africa','sudan','congo','sahel','mali','burkina','somalia'], asia:['china','taiwan','korea','japan','india','pakistan','myanmar'], breaking:['breaking','flash','urgent','alert'], }; function classify(title) { const t=title.toLowerCase(); for(const [cat,kws] of Object.entries(KEYWORDS)) { if(kws.some(k=>t.includes(k))) return cat; } return 'general'; } function sortByDate(a) { return a.sort((x,y)=>y.d-x.d); } // ── RSS Fetch ───────────────────────────────────────────── async function pool(tasks, n=6){ let i=0; const run = async()=>{ while(i x && (x.includes(' fn().then(x => { if(!isRSS(x)) throw 0; ctrl.abort(); return x; }); const tf = (ms, fn) => new Promise((res,rej) => { const id = setTimeout(() => rej('to'), ms); fn(sig).then(x=>{clearTimeout(id);res(x);}).catch(e=>{clearTimeout(id);rej(e);}); }); const after = ms => new Promise(r => setTimeout(r, ms)); try { return await Promise.any([ // 1. Worker Cloudflare — proxy sin CORS, cacheado 5min — PRIMERO Y SIN DELAY rss(() => tf(5000, s => fetch(WORKER+'/rss?url='+enc, {signal:s}) .then(r => { if(!r.ok) throw 0; return r.text(); }) )), // 2. Directo — feeds con CORS abierto (BBC, Al Jazeera, DW...) rss(() => tf(3000, s => fetch(url, {signal:s}).then(r => { if(!r.ok) throw 0; return r.text(); }) )), // 3. corsproxy.io — 400ms delay rss(() => after(400).then(() => tf(5000, s => fetch('https://corsproxy.io/?'+enc, {signal:s}) .then(r => { if(!r.ok) throw 0; return r.text(); }) ))), // 4. rss2json — 700ms delay, convierte JSON→XML rss(() => after(700).then(() => tf(6000, s => fetch('https://api.rss2json.com/v1/api.json?rss_url='+enc+'&count=25', {signal:s}) .then(r => { if(!r.ok) throw 0; return r.json(); }) .then(j => { if(!j.items?.length) throw 0; const items = j.items.map(i => '<![CDATA['+(i.title||'')+']]>'+ ''+(i.link||'')+''+ ''+(i.pubDate||'')+''+ '' ).join(''); return ''+(j.feed?.title||'')+''+items+''; }) ))), ]); } catch(e) { return null; } } async function fetchTG(ch){ try{ const r = await fetch(WORKER+'/telegram?ch='+encodeURIComponent(ch), {signal:AbortSignal.timeout(8000)}); if(!r.ok) return null; const d = await r.json(); return (d.items?.length) ? d : null; }catch(e){ return null; } } // ═══════════════════════════════════════════════════════════ // PARSE RSS // ═══════════════════════════════════════════════════════════ function parseRSS(xml, src, cat){ const items = []; // Extract text from a tag — strips all HTML, CDATA, entities const get = (b, tag) => { const m = b.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'i')); if(!m) return ''; return m[1] .replace(//gi, '') // CDATA .replace(/<[^>]+>/g, ' ') // strip tags .replace(/<[^&]*>/g, '') // encoded tags .replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,"'").replace(/ /g,' ') .replace(/\s+/g, ' ').trim(); }; // Clean desc — remove any remaining html-like junk, truncate cleanly const cleanDesc = raw => { if(!raw) return ''; // Strip anything that looks like html attribute junk let s = raw.replace(/\w+=["'][^"']*["']/g, '').replace(/\s+/g,' ').trim(); if(s.length < 20) return ''; // too short to be useful return s.slice(0, 160); }; let re = /]*>([\s\S]*?)<\/item>/gi, m; while((m=re.exec(xml))!==null){ const b=m[1], title=get(b,'title').slice(0,180); if(!title) continue; const link=get(b,'link')||get(b,'guid'); const pubDate=get(b,'pubDate')||get(b,'dc:date')||new Date().toUTCString(); const desc=cleanDesc(get(b,'description')); items.push({title,link,pubDate,desc,src,cat:cat==='general'?classify(title):cat,type:'rss'}); } if(!items.length){ re = /]*>([\s\S]*?)<\/entry>/gi; while((m=re.exec(xml))!==null){ const b=m[1], title=get(b,'title').slice(0,180); if(!title) continue; const link=(b.match(/]+href="([^"]+)"/)||[])[1]||''; const pubDate=get(b,'published')||get(b,'updated')||new Date().toUTCString(); const desc=cleanDesc(get(b,'summary')||get(b,'content')); items.push({title,link,pubDate,desc,src,cat:cat==='general'?classify(title):cat,type:'rss'}); } } return items; } // ═══════════════════════════════════════════════════════════ // MERGE — always newest first // ═══════════════════════════════════════════════════════════ const sortByDate = arr => arr.sort((a,b) => new Date(b.pubDate) - new Date(a.pubDate)); function merge(existing, incoming){ const seen = new Set(existing.map(i => i.link||i.title)); const fresh = incoming.filter(i => { const k=i.link||i.title; return k&&!seen.has(k); }); fresh.forEach(i => i._new=true); return { merged: sortByDate([...fresh,...existing]).slice(0,400), newCount: fresh.length }; } // ═══════════════════════════════════════════════════════════ // RENDER // ═══════════════════════════════════════════════════════════ function timeAgo(d){ if(!d) return ''; const s = Math.floor((Date.now()-new Date(d))/1000); if(s<60) return s+'s'; if(s<3600) return Math.floor(s/60)+'m'; if(s<86400) return Math.floor(s/3600)+'h'; return Math.floor(s/86400)+'d'; } function itemHTML(item){ const cat = item.type==='tg' ? 'tg' : classify(item.title); const [tcls,tlbl] = item.type==='twitter' ? ['tag-tw','𝕏 OSINT'] : item.type==='gdelt' ? ['tag-gd','GDELT'] : item.type==='osint' ? ['tag-tg','OSINT'] : (TAG_MAP[cat]||TAG_MAP.general); const href = (item.link||'').replace(/"/g,'"').replace(/'/g,'''); const isRead = readSet.has(item.link||''); const si = JSON.stringify({title:item.title,link:item.link,src:item.src,pubDate:item.pubDate}).replace(/"/g,'"'); return `
${tlbl} ${item.src} ${timeAgo(item.pubDate)}
${href?``:''} ${href?``:''}
${item.title.replace(//g,'>')}
${item.desc?`
${item.desc.replace(//g,'>')}
`:''}
`; } function renderFeed(items, feedId){ const el = document.getElementById(feedId); if(!el) return; const filt = feedId==='feed-media' ? activeCatMedia : activeCatOsint; let filtered = items; if(filt!=='all'){ if(filt==='breaking') filtered = items.filter(i=>KEYWORDS.breaking.test(i.title)); else filtered = items.filter(i=>(i.cat||'')==filt||(KEYWORDS[filt]&&KEYWORDS[filt].test(i.title))); } if(!filtered.length){ el.innerHTML='
Sin resultados para este filtro.
'; return; } // Build in fragment then swap — preserves scroll, no flash const scrollTop = el.scrollTop; const frag = document.createDocumentFragment(); const badge = el.querySelector('.nbadge'); if(badge) frag.appendChild(badge.cloneNode(true)); filtered.forEach(item => { const d = document.createElement('div'); d.innerHTML = itemHTML(item); if(d.firstChild) frag.appendChild(d.firstChild); }); el.innerHTML = ''; el.appendChild(frag); el.scrollTop = scrollTop; } function prependNew(items, feedId){ const el = document.getElementById(feedId); if(!el||!items.length) return; const ob = el.querySelector('.nbadge'); if(ob) ob.remove(); const badge = document.createElement('div'); badge.className = 'nbadge'; badge.innerHTML = `+${items.length} nuevas`; badge.onclick = ()=>badge.remove(); setTimeout(()=>{ if(badge.parentNode) badge.remove(); }, 8000); el.insertBefore(badge, el.firstChild); const frag = document.createDocumentFragment(); items.forEach((item,ix)=>{ const d = document.createElement('div'); d.innerHTML=itemHTML(item); const node = d.firstChild; node.style.animationDelay=(ix*.04)+'s'; frag.appendChild(node); }); el.insertBefore(frag, badge.nextSibling); } // ═══════════════════════════════════════════════════════════ // CONFLICTOS ACTIVOS — Lista curada, actualización manual // ═══════════════════════════════════════════════════════════ const CONFLICTS = [ // ── EUROPA ────────────────────────────────────────────── { id:'ukraine', name:'Guerra Rusia-Ucrania', region:'Europa', intensity:3, // 1=tensión 2=conflicto 3=guerra since:'Feb 2022', parties:'Rusia vs Ucrania + apoyo OTAN', deaths:'~750.000 (est.)', status:'Línea de frente activa en Donetsk, Zaporiyia y Járkov. Ataques con drones y misiles continuos sobre infraestructura civil.', tags:['guerra','invasión','europa'], }, // ── ORIENTE MEDIO ──────────────────────────────────────── { id:'gaza', name:'Guerra Gaza', region:'M.Este', intensity:3, since:'Oct 2023', parties:'Israel vs Hamas / PIJ', deaths:'>50.000 (Gaza)', status:'Operaciones terrestres en Rafah y norte de Gaza. Crisis humanitaria severa. Cese el fuego intermitente.', tags:['guerra','oriente medio'], }, { id:'westbank', name:'Cisjordania', region:'M.Este', intensity:2, since:'2023 (escal.)', parties:'Israel vs Autoridad Palestina / milicias', deaths:'>700 en 2024', status:'Operaciones militares israelíes en Jenin, Tulkarm y Yenín. Violencia de colonos en aumento.', tags:['conflicto','oriente medio'], }, { id:'lebanon', name:'Líbano (posguerra)', region:'M.Este', intensity:1, since:'Oct 2023', parties:'Israel vs Hezbolá', deaths:'>4.000 (2024)', status:'Alto el fuego vigente desde nov 2024. Retirada israelí pendiente. Hezbolá debilitado.', tags:['tensión','oriente medio'], }, { id:'yemen', name:'Yemen', region:'M.Este', intensity:2, since:'2015', parties:'Huzíes vs coalición / EE.UU.', deaths:'>150.000', status:'Huzíes atacan rutas marítimas en el Mar Rojo. EE.UU. y UK realizan bombardeos en respuesta.', tags:['conflicto','oriente medio'], }, { id:'syria', name:'Siria (posguerra)', region:'M.Este', intensity:1, since:'2011', parties:'HTS / SNA vs kurdos SDF', deaths:'>500.000 total', status:'Régimen Assad caído (dic 2024). HTS controla Damasco. Tensiones entre facciones en el norte.', tags:['tensión','oriente medio'], }, { id:'iran', name:'Irán — Tensión regional', region:'M.Este', intensity:1, since:'2024', parties:'Irán vs Israel / EE.UU.', deaths:'N/D', status:'Intercambios de misiles y drones en abr y oct 2024. Programa nuclear iraní bajo presión. Sanciones renovadas.', tags:['tensión','oriente medio'], }, // ── ÁFRICA ─────────────────────────────────────────────── { id:'sudan', name:'Guerra Civil Sudán', region:'África', intensity:3, since:'Abr 2023', parties:'FFAA sudanesas vs RSF (paramilitares)', deaths:'>150.000 (est.)', status:'Mayor crisis humanitaria del mundo. Darfur bajo control RSF. Jartum en disputa. Riesgo de hambruna masiva.', tags:['guerra','africa'], }, { id:'drcongo', name:'Este del Congo (RDC)', region:'África', intensity:3, since:'1998 (ciclos)', parties:'FARDC vs M23 / Rwanda', deaths:'>10.000 en 2024', status:'M23 tomó Goma en ene 2025. Kivu Norte bajo control rebelde. Apoyo rwandés documentado por la ONU.', tags:['guerra','africa'], }, { id:'sahel', name:'Sahel (Mali/Burkina/Níger)', region:'África', intensity:2, since:'2012', parties:'JNIM / ISGS vs ejércitos juntas + Wagner', deaths:'>10.000/año', status:'JNIM controla amplias zonas rurales. Juntas expulsan tropas francesas. Wagner opera en Mali y Burkina.', tags:['conflicto','africa'], }, { id:'somalia', name:'Somalia', region:'África', intensity:2, since:'2006', parties:'Al-Shabaab vs Gobierno / ATMIS', deaths:'>5.000/año', status:'Al-Shabaab controla zonas del sur y centro. Ataques continuos en Mogadiscio. Retirada ATMIS en curso.', tags:['conflicto','africa'], }, { id:'ethiopia', name:'Etiopía (Amhara/Oromía)', region:'África', intensity:2, since:'2023', parties:'Ejército etíope vs FANO (Amhara) / OLA', deaths:'>5.000 (2024)', status:'Conflicto en Amhara y Oromía persiste. Tigray en frágil paz desde nov 2022. Acceso humanitario bloqueado.', tags:['conflicto','africa'], }, { id:'mozambique', name:'Mozambique Norte', region:'África', intensity:2, since:'2017', parties:'Ansar al-Sunna (ISIS-M) vs FFAA + Rwanda', deaths:'>6.000', status:'Cabo Delgado: insurgencia islámica activa. Fuerzas rwandesas y SAMIM contienen avance. Civiles desplazados.', tags:['conflicto','africa'], }, // ── ASIA ───────────────────────────────────────────────── { id:'myanmar', name:'Myanmar (Guerra Civil)', region:'Asia', intensity:3, since:'Feb 2021', parties:'Junta (SAC) vs PDF / EAOs', deaths:'>50.000', status:'Alianza de Tres Hermandades controla gran parte del norte. Junta pierde territorio. Sin perspectiva de paz.', tags:['guerra','asia'], }, { id:'pakistan', name:'Pakistán (KPK/Baluchistán)', region:'Asia', intensity:2, since:'2007 (rebrote)', parties:'Ejército paquistaní vs TTP / BLA', deaths:'>1.000/año', status:'TTP intensifica ataques desde Afganistán. BLA activa en Baluchistán. Tensiones con Kabul.', tags:['conflicto','asia'], }, { id:'taiwan', name:'Estrecho de Taiwán', region:'Asia', intensity:1, since:'Permanente', parties:'China (RPC) vs Taiwán (ROC)', deaths:'N/D', status:'Ejercicios militares chinos frecuentes. Incursiones aéreas en zona ADIZ. Sin conflicto armado activo.', tags:['tensión','asia'], }, { id:'northkorea', name:'Corea del Norte', region:'Asia', intensity:1, since:'Permanente', parties:'RPDC vs Corea del Sur / EE.UU.', deaths:'N/D', status:'Ensayos de misiles balísticos continuos. Tropas norcoreanas desplegadas en Rusia (Ucrania).', tags:['tensión','asia'], }, ]; let _cfFilter = 'all'; const CF_INTENSITY = { 3: {label:'GUERRA', col:'var(--red)', dot:'var(--red)', pulse:true}, 2: {label:'CONFLICTO', col:'var(--amb)', dot:'var(--amb)', pulse:false}, 1: {label:'TENSIÓN', col:'var(--t1)', dot:'var(--t1)', pulse:false}, }; function cfFilter(region, btn){ _cfFilter = region; document.querySelectorAll('#cf-filters .fbtn').forEach(b => b.classList.remove('on')); if(btn) btn.classList.add('on'); renderConflicts(); } function renderConflicts(){ const list = document.getElementById('cf-list'); const cnt = document.getElementById('cf-count'); const upd = document.getElementById('cf-updated'); if(!list) return; const data = _cfFilter === 'all' ? CONFLICTS : CONFLICTS.filter(c => c.region === _cfFilter); if(cnt) cnt.textContent = data.length + ' ACT'; // Group by intensity const byInt = {3:[], 2:[], 1:[]}; data.forEach(c => (byInt[c.intensity]||byInt[1]).push(c)); let html = ''; [3,2,1].forEach(i => { if(!byInt[i].length) return; const {label, col} = CF_INTENSITY[i]; html += `
${label} · ${byInt[i].length}
`; byInt[i].forEach(cf => { const {dot, pulse} = CF_INTENSITY[cf.intensity]; html += `
${cf.name}
${cf.parties}
`; }); }); list.innerHTML = html; if(upd){ const now = new Date(); upd.textContent = now.toLocaleDateString('es',{month:'short',year:'numeric'}).toUpperCase(); } } function cfExpand(id){ const el = document.getElementById('cf-expand-'+id); if(!el) return; const open = el.style.display !== 'none'; // Close all others document.querySelectorAll('[id^="cf-expand-"]').forEach(e => e.style.display='none'); if(!open) el.style.display = 'block'; } // ═══════════════════════════════════════════════════════════ // LOAD MEDIA // ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════ // PROGRESS + STATUS HELPERS // ═══════════════════════════════════════════════════════════ function progSet(pct){ const f=document.getElementById('prog-fill'); const b=document.getElementById('prog-bar'); if(!f||!b) return; b.classList.remove('idle'); f.style.width=pct+'%'; if(pct>=100){ setTimeout(()=>{b.classList.add('idle');setTimeout(()=>{b.classList.remove('idle');f.style.width='0%';},700);},200); } } function colStatus(id,state,text=''){ const el=document.getElementById('cs-'+id); if(!el) return; if(state==='loading') el.innerHTML=''; else if(state==='ok') el.innerHTML=''+(text?' '+text+'':''); else if(state==='err') el.innerHTML=''; else el.innerHTML=''; } function setLastUpdated(){ const el=document.getElementById('last-updated'); if(!el) return; const n=new Date(); el.textContent='Actualizado '+String(n.getUTCHours()).padStart(2,'0')+':'+String(n.getUTCMinutes()).padStart(2,'0')+' UTC'; } // ═══════════════════════════════════════════════════════════ // LOAD MEDIA // ═══════════════════════════════════════════════════════════ // ── Feed item render ────────────────────────────────────── function itemHTML(item){ const cat = item.type==='tg' ? 'tg' : classify(item.title); const [tcls,tlbl] = item.type==='twitter' ? ['tag-tw','𝕏 OSINT'] : item.type==='gdelt' ? ['tag-gd','GDELT'] : item.type==='osint' ? ['tag-tg','OSINT'] : (TAG_MAP[cat]||TAG_MAP.general); const href = (item.link||'').replace(/"/g,'"').replace(/'/g,'''); const isRead = readSet.has(item.link||''); const si = JSON.stringify({title:item.title,link:item.link,src:item.src,pubDate:item.pubDate}).replace(/"/g,'"'); return `
${tlbl} ${item.src} ${timeAgo(item.pubDate)}
${href?``:''} ${href?``:''}
${item.title.replace(//g,'>')}
${item.desc?`
${item.desc.replace(//g,'>')}
`:''}
`; } function renderFeed(items, feedId){ const el = document.getElementById(feedId); if(!el) return; const filt = feedId==='feed-media' ? activeCatMedia : activeCatOsint; let filtered = items; if(filt!=='all'){ if(filt==='breaking') filtered = items.filter(i=>KEYWORDS.breaking.test(i.title)); else filtered = items.filter(i=>(i.cat||'')==filt||(KEYWORDS[filt]&&KEYWORDS[filt].test(i.title))); } if(!filtered.length){ el.innerHTML='
Sin resultados para este filtro.
'; return; } // Build in fragment then swap — preserves scroll, no flash const scrollTop = el.scrollTop; const frag = document.createDocumentFragment(); const badge = el.querySelector('.nbadge'); if(badge) frag.appendChild(badge.cloneNode(true)); filtered.forEach(item => { const d = document.createElement('div'); d.innerHTML = itemHTML(item); if(d.firstChild) frag.appendChild(d.firstChild); }); el.innerHTML = ''; el.appendChild(frag); el.scrollTop = scrollTop; } function prependNew(items, feedId){ const el = document.getElementById(feedId); if(!el||!items.length) return; const ob = el.querySelector('.nbadge'); if(ob) ob.remove(); const badge = document.createElement('div'); badge.className = 'nbadge'; badge.innerHTML = `+${items.length} nuevas`; badge.onclick = ()=>badge.remove(); setTimeout(()=>{ if(badge.parentNode) badge.remove(); }, 8000); el.insertBefore(badge, el.firstChild); const frag = document.createDocumentFragment(); items.forEach((item,ix)=>{ const d = document.createElement('div'); d.innerHTML=itemHTML(item); const node = d.firstChild; node.style.animationDelay=(ix*.04)+'s'; frag.appendChild(node); }); el.insertBefore(frag, badge.nextSibling); } // ═══════════════════════════════════════════════════════════ // CONFLICTOS ACTIVOS — Lista curada, actualización manual // ═══════════════════════════════════════════════════════════ const CONFLICTS = [ // ── EUROPA ────────────────────────────────────────────── { id:'ukraine', name:'Guerra Rusia-Ucrania', region:'Europa', intensity:3, // 1=tensión 2=conflicto 3=guerra since:'Feb 2022', parties:'Rusia vs Ucrania + apoyo OTAN', deaths:'~750.000 (est.)', status:'Línea de frente activa en Donetsk, Zaporiyia y Járkov. Ataques con drones y misiles continuos sobre infraestructura civil.', tags:['guerra','invasión','europa'], }, // ── ORIENTE MEDIO ──────────────────────────────────────── { id:'gaza', name:'Guerra Gaza', region:'M.Este', intensity:3, since:'Oct 2023', parties:'Israel vs Hamas / PIJ', deaths:'>50.000 (Gaza)', status:'Operaciones terrestres en Rafah y norte de Gaza. Crisis humanitaria severa. Cese el fuego intermitente.', tags:['guerra','oriente medio'], }, { id:'westbank', name:'Cisjordania', region:'M.Este', intensity:2, since:'2023 (escal.)', parties:'Israel vs Autoridad Palestina / milicias', deaths:'>700 en 2024', status:'Operaciones militares israelíes en Jenin, Tulkarm y Yenín. Violencia de colonos en aumento.', tags:['conflicto','oriente medio'], }, { id:'lebanon', name:'Líbano (posguerra)', region:'M.Este', intensity:1, since:'Oct 2023', parties:'Israel vs Hezbolá', deaths:'>4.000 (2024)', status:'Alto el fuego vigente desde nov 2024. Retirada israelí pendiente. Hezbolá debilitado.', tags:['tensión','oriente medio'], }, { id:'yemen', name:'Yemen', region:'M.Este', intensity:2, since:'2015', parties:'Huzíes vs coalición / EE.UU.', deaths:'>150.000', status:'Huzíes atacan rutas marítimas en el Mar Rojo. EE.UU. y UK realizan bombardeos en respuesta.', tags:['conflicto','oriente medio'], }, { id:'syria', name:'Siria (posguerra)', region:'M.Este', intensity:1, since:'2011', parties:'HTS / SNA vs kurdos SDF', deaths:'>500.000 total', status:'Régimen Assad caído (dic 2024). HTS controla Damasco. Tensiones entre facciones en el norte.', tags:['tensión','oriente medio'], }, { id:'iran', name:'Irán — Tensión regional', region:'M.Este', intensity:1, since:'2024', parties:'Irán vs Israel / EE.UU.', deaths:'N/D', status:'Intercambios de misiles y drones en abr y oct 2024. Programa nuclear iraní bajo presión. Sanciones renovadas.', tags:['tensión','oriente medio'], }, // ── ÁFRICA ─────────────────────────────────────────────── { id:'sudan', name:'Guerra Civil Sudán', region:'África', intensity:3, since:'Abr 2023', parties:'FFAA sudanesas vs RSF (paramilitares)', deaths:'>150.000 (est.)', status:'Mayor crisis humanitaria del mundo. Darfur bajo control RSF. Jartum en disputa. Riesgo de hambruna masiva.', tags:['guerra','africa'], }, { id:'drcongo', name:'Este del Congo (RDC)', region:'África', intensity:3, since:'1998 (ciclos)', parties:'FARDC vs M23 / Rwanda', deaths:'>10.000 en 2024', status:'M23 tomó Goma en ene 2025. Kivu Norte bajo control rebelde. Apoyo rwandés documentado por la ONU.', tags:['guerra','africa'], }, { id:'sahel', name:'Sahel (Mali/Burkina/Níger)', region:'África', intensity:2, since:'2012', parties:'JNIM / ISGS vs ejércitos juntas + Wagner', deaths:'>10.000/año', status:'JNIM controla amplias zonas rurales. Juntas expulsan tropas francesas. Wagner opera en Mali y Burkina.', tags:['conflicto','africa'], }, { id:'somalia', name:'Somalia', region:'África', intensity:2, since:'2006', parties:'Al-Shabaab vs Gobierno / ATMIS', deaths:'>5.000/año', status:'Al-Shabaab controla zonas del sur y centro. Ataques continuos en Mogadiscio. Retirada ATMIS en curso.', tags:['conflicto','africa'], }, { id:'ethiopia', name:'Etiopía (Amhara/Oromía)', region:'África', intensity:2, since:'2023', parties:'Ejército etíope vs FANO (Amhara) / OLA', deaths:'>5.000 (2024)', status:'Conflicto en Amhara y Oromía persiste. Tigray en frágil paz desde nov 2022. Acceso humanitario bloqueado.', tags:['conflicto','africa'], }, { id:'mozambique', name:'Mozambique Norte', region:'África', intensity:2, since:'2017', parties:'Ansar al-Sunna (ISIS-M) vs FFAA + Rwanda', deaths:'>6.000', status:'Cabo Delgado: insurgencia islámica activa. Fuerzas rwandesas y SAMIM contienen avance. Civiles desplazados.', tags:['conflicto','africa'], }, // ── ASIA ───────────────────────────────────────────────── { id:'myanmar', name:'Myanmar (Guerra Civil)', region:'Asia', intensity:3, since:'Feb 2021', parties:'Junta (SAC) vs PDF / EAOs', deaths:'>50.000', status:'Alianza de Tres Hermandades controla gran parte del norte. Junta pierde territorio. Sin perspectiva de paz.', tags:['guerra','asia'], }, { id:'pakistan', name:'Pakistán (KPK/Baluchistán)', region:'Asia', intensity:2, since:'2007 (rebrote)', parties:'Ejército paquistaní vs TTP / BLA', deaths:'>1.000/año', status:'TTP intensifica ataques desde Afganistán. BLA activa en Baluchistán. Tensiones con Kabul.', tags:['conflicto','asia'], }, { id:'taiwan', name:'Estrecho de Taiwán', region:'Asia', intensity:1, since:'Permanente', parties:'China (RPC) vs Taiwán (ROC)', deaths:'N/D', status:'Ejercicios militares chinos frecuentes. Incursiones aéreas en zona ADIZ. Sin conflicto armado activo.', tags:['tensión','asia'], }, { id:'northkorea', name:'Corea del Norte', region:'Asia', intensity:1, since:'Permanente', parties:'RPDC vs Corea del Sur / EE.UU.', deaths:'N/D', status:'Ensayos de misiles balísticos continuos. Tropas norcoreanas desplegadas en Rusia (Ucrania).', tags:['tensión','asia'], }, ]; let _cfFilter = 'all'; const CF_INTENSITY = { 3: {label:'GUERRA', col:'var(--red)', dot:'var(--red)', pulse:true}, 2: {label:'CONFLICTO', col:'var(--amb)', dot:'var(--amb)', pulse:false}, 1: {label:'TENSIÓN', col:'var(--t1)', dot:'var(--t1)', pulse:false}, }; function cfFilter(region, btn){ _cfFilter = region; document.querySelectorAll('#cf-filters .fbtn').forEach(b => b.classList.remove('on')); if(btn) btn.classList.add('on'); renderConflicts(); } function renderConflicts(){ const list = document.getElementById('cf-list'); const cnt = document.getElementById('cf-count'); const upd = document.getElementById('cf-updated'); if(!list) return; const data = _cfFilter === 'all' ? CONFLICTS : CONFLICTS.filter(c => c.region === _cfFilter); if(cnt) cnt.textContent = data.length + ' ACT'; // Group by intensity const byInt = {3:[], 2:[], 1:[]}; data.forEach(c => (byInt[c.intensity]||byInt[1]).push(c)); let html = ''; [3,2,1].forEach(i => { if(!byInt[i].length) return; const {label, col} = CF_INTENSITY[i]; html += `
${label} · ${byInt[i].length}
`; byInt[i].forEach(cf => { const {dot, pulse} = CF_INTENSITY[cf.intensity]; html += `
${cf.name}
${cf.parties}
`; }); }); list.innerHTML = html; if(upd){ const now = new Date(); upd.textContent = now.toLocaleDateString('es',{month:'short',year:'numeric'}).toUpperCase(); } } function cfExpand(id){ const el = document.getElementById('cf-expand-'+id); if(!el) return; const open = el.style.display !== 'none'; // Close all others document.querySelectorAll('[id^="cf-expand-"]').forEach(e => e.style.display='none'); if(!open) el.style.display = 'block'; } // ═══════════════════════════════════════════════════════════ // LOAD MEDIA // ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════ // PROGRESS + STATUS HELPERS // ═══════════════════════════════════════════════════════════ function progSet(pct){ const f=document.getElementById('prog-fill'); const b=document.getElementById('prog-bar'); if(!f||!b) return; b.classList.remove('idle'); f.style.width=pct+'%'; if(pct>=100){ setTimeout(()=>{b.classList.add('idle');setTimeout(()=>{b.classList.remove('idle');f.style.width='0%';},700);},200); } } function colStatus(id,state,text=''){ const el=document.getElementById('cs-'+id); if(!el) return; if(state==='loading') el.innerHTML=''; else if(state==='ok') el.innerHTML=''+(text?' '+text+'':''); else if(state==='err') el.innerHTML=''; else el.innerHTML=''; } function setLastUpdated(){ const el=document.getElementById('last-updated'); if(!el) return; const n=new Date(); el.textContent='Actualizado '+String(n.getUTCHours()).padStart(2,'0')+':'+String(n.getUTCMinutes()).padStart(2,'0')+' UTC'; } // ═══════════════════════════════════════════════════════════ // LOAD MEDIA // ═══════════════════════════════════════════════════════════ // ── Load feeds ──────────────────────────────────────────── async function loadMedia(){ colStatus('media','loading'); const srcs=[...MEDIA_SOURCES,...customSources]; let done=0, firstRender=false; const all=[]; const isFirst=!mediaCache.data.length; await pool(srcs.map(src=>async()=>{ const xml = await fetchRSS(src.url); if(xml){ parseRSS(xml,src.n,src.cat).forEach(i=>all.push(i)); // Show first results as soon as 5+ items arrive — don't wait for all sources if(isFirst && !firstRender && all.length >= 5){ firstRender=true; sortByDate(all); mediaCache={ts:Date.now(),data:[...all]}; renderFeed(all,'feed-media'); colStatus('media','ok',all.length+' items'); const e=document.getElementById('ft-media'); if(e) e.textContent=all.length; } } done++; progSet(Math.round((done/srcs.length)*50)); }),15); sortByDate(all); if(!all.length){ if(isFirst) document.getElementById('feed-media').innerHTML='
Sin resultados. Comprueba tu conexión.
'; colStatus('media','err'); return; } if(isFirst){ mediaCache={ts:Date.now(),data:all}; renderFeed(all,'feed-media'); } else { const {merged,newCount}=merge(mediaCache.data,all); mediaCache={ts:Date.now(),data:merged}; if(newCount) prependNew(merged.slice(0,newCount),'feed-media'); else renderFeed(merged,'feed-media'); } const e=document.getElementById('ft-media'); if(e) e.textContent=mediaCache.data.length; colStatus('media','ok',mediaCache.data.length+' items'); syncMobBadges(); } // ═══════════════════════════════════════════════════════════ // LOAD OSINT // ═══════════════════════════════════════════════════════════ async function loadOSINT(){ colStatus('osint','loading'); let done=0; const all=[]; const isFirst=!osintCache.data.length; let firstRender=false; await pool(OSINT_SOURCES.map(src=>async()=>{ const xml=await fetchRSS(src.url); if(xml){ const items = parseRSS(xml,src.n,src.cat); items.forEach(i=>{i.type='osint';all.push(i);}); if(isFirst && !firstRender && all.length >= 3){ firstRender = true; sortByDate(all); osintCache={ts:Date.now(),data:[...all]}; renderFeed(all,'feed-osint'); colStatus('osint','ok', all.length+' items'); } } done++; progSet(50+Math.round((done/OSINT_SOURCES.length)*50)); }),10); sortByDate(all); if(!all.length&&isFirst){ document.getElementById('feed-osint').innerHTML='
Sin datos OSINT disponibles.
'; colStatus('osint','err'); return; } if(isFirst){ osintCache={ts:Date.now(),data:all}; renderFeed(all,'feed-osint'); } else { const {merged,newCount}=merge(osintCache.data,all); osintCache={ts:Date.now(),data:merged}; if(newCount) prependNew(merged.slice(0,newCount),'feed-osint'); else renderFeed(merged,'feed-osint'); } const e=document.getElementById('ft-osint'); if(e) e.textContent=osintCache.data.length; colStatus('osint','ok',osintCache.data.length+' items'); progSet(100); } // Load Twitter/X via RSSHub — silently merges into OSINT // ═══════════════════════════════════════════════════════════ // BREAKING BAR // ═══════════════════════════════════════════════════════════ async function loadBreakingBar(){ // Reuse media cache if available — no extra fetches if(brkLoaded&&mediaCache.data.length){ _renderBreakingBar(mediaCache.data); return; } const all=[]; await pool(BREAKING_RSS.map(url=>async()=>{ const xml=await fetchRSS(url); if(!xml) return; parseRSS(xml,'','general').slice(0,10).forEach(i=>all.push(i)); }),3); sortByDate(all); if(all.length){ brkLoaded=true; _renderBreakingBar(all); } } function _renderBreakingBar(items){ const inner=document.getElementById('brk-inner'); if(!inner) return; const kw=/war|conflict|attack|killed|missile|strike|troops|offensive|bomb|crisis|explosion/i; const filtered=items.filter(i=>kw.test(i.title)); const display=(filtered.length>=5?filtered:items).slice(0,40); const html=display.map(i=>{ const href=(i.link||'').replace(/'/g,"\\'"); return `${i.title}`; }).join(''); inner.innerHTML=html+html; requestAnimationFrame(()=>{ inner.style.animationDuration=Math.max(40,inner.scrollWidth/2/110)+'s'; }); } // ═══════════════════════════════════════════════════════════ // REFRESH ALL — debounced, single entry point // ═══════════════════════════════════════════════════════════ async function refreshAll(){ if(_refreshing) return; _refreshing=true; const btn=document.getElementById('refresh-btn'); if(btn){btn.classList.add('active');btn.disabled=true;} progSet(2); mediaCache={ts:0,data:[]}; osintCache={ts:0,data:[]}; gdeltCache={ts:0,data:[]}; brkLoaded=false; const _fm=document.getElementById('feed-media'); const _fo=document.getElementById('feed-osint'); // Keep existing items visible during refresh — no blanking, no flash try{ await Promise.all([loadMedia(),loadOSINT()]); setLastUpdated(); // feeds already updated in place by loadMedia/loadOSINT loadBreakingBar(); loadGDELT(); }finally{ _refreshing=false; if(btn){btn.classList.remove('active');btn.disabled=false;} } } // ═══════════════════════════════════════════════════════════ // AUTO-REFRESH — tab-aware, 5min interval, no countdown // ═══════════════════════════════════════════════════════════ function startAutoRefresh(){ const INTERVAL=5*60*1000; let lastRun=Date.now(); function schedule(){ const wait=Math.max(0,INTERVAL-(Date.now()-lastRun)); setTimeout(async()=>{ if(!document.hidden){lastRun=Date.now();await refreshAll();} schedule(); },wait); } document.addEventListener('visibilitychange',()=>{ if(!document.hidden&&Date.now()-lastRun>INTERVAL){lastRun=Date.now();refreshAll();} }); schedule(); } // ═══════════════════════════════════════════════════════════ // ITEM ACTIONS // ═══════════════════════════════════════════════════════════ function openItem(href){ window.open(href,'_blank'); markRead(href); } function markRead(href){ if(!href) return; readSet.add(href); try{ localStorage.setItem('ct_read', JSON.stringify([...readSet].slice(-500))); }catch(e){} // Visually mark document.querySelectorAll('.fitem').forEach(el => { if(el.getAttribute('onclick')?.includes(href)) el.classList.add('read'); }); } function toggleRead(btn, href){ const item = btn.closest('.fitem'); if(readSet.has(href)){ readSet.delete(href); item.classList.remove('read'); btn.classList.remove('read-on'); } else { readSet.add(href); item.classList.add('read'); btn.classList.add('read-on'); } try{ localStorage.setItem('ct_read', JSON.stringify([...readSet].slice(-500))); }catch(e){} } function saveItem(item){ if(savedItems.find(s=>s.link===item.link)) return; savedItems.unshift(item); if(savedItems.length>100) savedItems.pop(); try{ localStorage.setItem('ct_saved', JSON.stringify(savedItems)); }catch(e){} } // ═══════════════════════════════════════════════════════════ // SIDEBAR MODES // ═══════════════════════════════════════════════════════════ function setSidebar(mode, btn){ currentSidebar = mode; document.querySelectorAll('.sb-btn').forEach(b=>b.classList.remove('on')); btn.classList.add('on'); const media = document.getElementById('feed-media'); const saved = document.getElementById('feed-saved'); const fm = document.getElementById('filters-media'); if(mode==='saved'){ media.style.display='none'; if(fm) fm.style.display='none'; saved.style.display=''; renderSaved(); } else { media.style.display=''; if(fm) fm.style.display=''; saved.style.display='none'; } } function renderSaved(){ const el = document.getElementById('feed-saved'); if(!el) return; if(!savedItems.length){ el.innerHTML='
Sin guardadas.
'; return; } el.innerHTML = savedItems.map((item,i) => `
${item.title}
${item.src}
`).join(''); } // ═══════════════════════════════════════════════════════════ // FILTER FEEDS // ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════ // TWITTER — RSSHub feed (same engine as OSINT) // ═══════════════════════════════════════════════════════════ function filterFeed(f, btn, which){ if(which==='media') activeCatMedia=f; else activeCatOsint=f; btn.closest('.filters').querySelectorAll('.fbtn').forEach(b=>b.classList.remove('on')); btn.classList.add('on'); if(which==='media') renderFeed(mediaCache.data, 'feed-media'); else renderFeed(osintCache.data, 'feed-osint'); } // ═══════════════════════════════════════════════════════════ // PANELS // ═══════════════════════════════════════════════════════════ function openPanel(id){ closePanel(); activePanel = id; document.getElementById('panel-overlay').style.display = 'block'; document.getElementById('panel-'+id)?.classList.add('open'); } function closePanel(){ document.querySelectorAll('.side-panel').forEach(p=>p.classList.remove('open')); document.getElementById('panel-overlay').style.display = 'none'; activePanel = null; } // ═══════════════════════════════════════════════════════════ // OREF // ═══════════════════════════════════════════════════════════ async function loadOREF(){ try{ const r = await fetch(WORKER+'/oref', {signal:AbortSignal.timeout(5000)}); if(!r.ok) return; const d = await r.json(); const banner = document.getElementById('oref-banner'); if(d.status==='active' && d.alerts?.length){ banner.classList.add('active'); document.getElementById('oref-text').textContent = '🚨 ALERTA COHETES — ' + d.alerts.map(a=>a.data||'').flat().join(' · '); } else { banner.classList.remove('active'); } }catch(e){} } // ═══════════════════════════════════════════════════════════ // PREFS // ═══════════════════════════════════════════════════════════ function applyFont(v){ // v is the raw option value e.g. "Inter" or "'JetBrains Mono'" document.documentElement.style.setProperty('--font', v + ',sans-serif'); localStorage.setItem('ct_font', v); const s = document.getElementById('pref-font'); if(s) s.value = v; } function applySize(v){ v = parseInt(v); document.documentElement.style.fontSize = v+'px'; const sv = document.getElementById('size-val'); if(sv) sv.textContent=v+'px'; const sl = document.getElementById('pref-size'); if(sl) sl.value=v; localStorage.setItem('ct_size', v); } function applyAccent(v, dot){ document.documentElement.style.setProperty('--blue', v); document.documentElement.style.setProperty('--accent', v); localStorage.setItem('ct_accent', v); document.querySelectorAll('.cdot').forEach(d=>d.classList.remove('on')); if(dot) dot.classList.add('on'); } function toggleTheme(){ document.body.classList.toggle('light'); const isLight = document.body.classList.contains('light'); const btn = document.getElementById('theme-btn'); const tog = document.getElementById('tog-dark'); if(tog) tog.classList.toggle('on', !isLight); localStorage.setItem('ct_theme', isLight?'light':'dark'); } function resetPrefs(){ ['ct_font','ct_size','ct_accent','ct_theme'].forEach(k=>localStorage.removeItem(k)); location.reload(); } function loadPrefs(){ const font = localStorage.getItem('ct_font'); if(font) applyFont(font); const size = localStorage.getItem('ct_size'); if(size) applySize(size); const accent = localStorage.getItem('ct_accent'); if(accent){ document.documentElement.style.setProperty('--blue', accent); document.querySelectorAll('.cdot').forEach(d => { if(d.style.background===accent||d.getAttribute('onclick')?.includes(accent)){ d.classList.add('on'); } else d.classList.remove('on'); }); } if(localStorage.getItem('ct_theme')==='light'){ document.body.classList.add('light'); const tog=document.getElementById('tog-dark'); if(tog) tog.classList.remove('on'); } } // ═══════════════════════════════════════════════════════════ // CUSTOM SOURCES // ═══════════════════════════════════════════════════════════ function addSource(){ const name = document.getElementById('src-name').value.trim(); const url = document.getElementById('src-url').value.trim(); const cat = document.getElementById('src-cat').value; if(!name||!url) return; customSources.push({n:name, url, cat}); localStorage.setItem('ct_custom', JSON.stringify(customSources)); document.getElementById('src-name').value = ''; document.getElementById('src-url').value = ''; renderCustomList(); } function removeSource(i){ customSources.splice(i,1); localStorage.setItem('ct_custom', JSON.stringify(customSources)); renderCustomList(); } function renderCustomList(){ const el = document.getElementById('custom-list'); if(!el) return; if(!customSources.length){ el.innerHTML='
Sin fuentes personalizadas.
'; return; } el.innerHTML = customSources.map((s,i) => `
${s.n} ${s.cat}
`).join(''); } // ═══════════════════════════════════════════════════════════ // CLOCK // ═══════════════════════════════════════════════════════════ function startClock(){ const el = document.getElementById('clock'); setInterval(()=>{ const n=new Date(); if(el) el.textContent = String(n.getUTCHours()).padStart(2,'0')+':'+ String(n.getUTCMinutes()).padStart(2,'0')+':'+ String(n.getUTCSeconds()).padStart(2,'0')+' UTC'; },1000); } // ═══════════════════════════════════════════════════════════ // INIT // ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════ // ADMIN — Map editor (password protected) // ═══════════════════════════════════════════════════════════ // Admin password — stored as SHA-256 hash // Default password: "conflicttracker2025" // To change: run btoa(await crypto.subtle.digest('SHA-256', new TextEncoder().encode('newpassword'))) const ADMIN_PWD_HASH = 'f7e29ca28134088a79623d800bf1204d14032dd0c445bf569e91fcdd23f1c4d4'; let _drawLayers = []; // saved drawing layers // ── Admin: Publish & Sync ───────────────────────────────── // ── Feed filter ─────────────────────────────────────────── function filterFeed(f, btn, which) { if(which==='media') activeCatMedia=f; else activeCatOsint=f; document.querySelectorAll('#filters-'+which+' .fbtn').forEach(b=>b.classList.remove('on')); btn.classList.add('on'); const data=which==='media'?mediaCache.data:osintCache.data; const id=which==='media'?'feed-media':'feed-osint'; const filtered=f==='all'?data:f==='breaking'?data.filter(x=>x.tag==='breaking'||x.tag==='conflict'):data.filter(x=>x.cat===f||x.tag===f); const el=document.getElementById(id); if(el) el.innerHTML=filtered.length?filtered.slice(0,80).map(itemHTML).join(''):'
No results
'; } // ── Breaking bar ────────────────────────────────────────── function updateBreakingBar(items) { if(!items||!items.length) return; const html=items.map(x=>`${x.title}`).join(''); const inner=document.getElementById('brk-inner'); if(inner) inner.innerHTML=html+html; } // ── Conflict list ───────────────────────────────────────── let _cfFilter = 'all'; const CF_INTENSITY = { 3: {label:'GUERRA', col:'var(--red)', dot:'var(--red)', pulse:true}, 2: {label:'CONFLICTO', col:'var(--amb)', dot:'var(--amb)', pulse:false}, 1: {label:'TENSIÓN', col:'var(--t1)', dot:'var(--t1)', pulse:false}, }; function cfFilter(region, btn){ _cfFilter = region; document.querySelectorAll('#cf-filters .fbtn').forEach(b => b.classList.remove('on')); if(btn) btn.classList.add('on'); renderConflicts(); } function renderConflicts(){ const list = document.getElementById('cf-list'); const cnt = document.getElementById('cf-count'); const upd = document.getElementById('cf-updated'); if(!list) return; const data = _cfFilter === 'all' ? CONFLICTS : CONFLICTS.filter(c => c.region === _cfFilter); if(cnt) cnt.textContent = data.length + ' ACT'; // Group by intensity const byInt = {3:[], 2:[], 1:[]}; data.forEach(c => (byInt[c.intensity]||byInt[1]).push(c)); let html = ''; [3,2,1].forEach(i => { if(!byInt[i].length) return; const {label, col} = CF_INTENSITY[i]; html += `
${label} · ${byInt[i].length}
`; byInt[i].forEach(cf => { const {dot, pulse} = CF_INTENSITY[cf.intensity]; html += `
${cf.name}
${cf.parties}
`; }); }); list.innerHTML = html; if(upd){ const now = new Date(); upd.textContent = now.toLocaleDateString('es',{month:'short',year:'numeric'}).toUpperCase(); } } function cfExpand(id){ const el = document.getElementById('cf-expand-'+id); if(!el) return; const open = el.style.display !== 'none'; // Close all others document.querySelectorAll('[id^="cf-expand-"]').forEach(e => e.style.display='none'); if(!open) el.style.display = 'block'; } // ── Admin / GitHub sync ─────────────────────────────────── async function adminPublish(){ const btn = document.getElementById('admin-publish-btn'); const resetBtn = () => { if(btn){ btn.disabled=false; btn.innerHTML='↑ Publicar cambios'; } }; if(btn){ btn.disabled=true; btn.innerHTML='Publicando...'; } if(!ADMIN_SECRET){ document.getElementById('admin-jsonbin-setup').style.display = 'block'; _showSyncStatus('⚠ ADMIN_SECRET no configurado', 'var(--amb)'); resetBtn(); return; } try{ const data = _getConflicts(); const ok = await _saveConflicts(data); _showSyncStatus( ok ? '✓ Publicado — '+data.length+' conflictos en GitHub' : '✗ Error — revisa la consola (F12)', ok ? 'var(--grn)' : 'var(--red)' ); }catch(e){ console.error('adminPublish error:', e); _showSyncStatus('✗ Error: '+e.message, 'var(--red)'); } finally { resetBtn(); } } function _showSyncStatus(msg, col){ col = col || 'var(--grn)'; const el = document.getElementById('admin-sync-status'); if(!el) return; el.textContent = msg; el.style.color = col; setTimeout(() => { if(el.textContent===msg) el.textContent=''; }, 5000); } async function _pollRemoteConflicts(){ if(_adminAuthed) return; try{ const r = await fetch(WORKER_CONFLICTS_GET + '?t=' + Math.floor(Date.now()/60000), { signal: AbortSignal.timeout(6000), }); if(!r.ok) return; const data = await r.json(); if(!Array.isArray(data)) return; if(!Array.isArray(data) || !data.length) return; const current = JSON.stringify(_conflicts); if(JSON.stringify(data) !== current){ _conflicts = data; localStorage.setItem(SK_CONFLICTS, JSON.stringify(data)); _drawConflicts(); } }catch(e){} } // Storage keys const SK_CONFLICTS = 'ct_map_conflicts'; const SK_DRAWINGS = 'ct_map_drawings'; // ── Open admin panel ────────────────────────────────────── function openAdminPanel(){ openPanel('admin'); if(_adminAuthed){ document.getElementById('admin-login').style.display = 'none'; document.getElementById('admin-workspace').style.display = 'flex'; _renderAdminList(); _loadDrawingsFromStorage(); } } // ── Login ───────────────────────────────────────────────── async function adminLogin(){ const pwd = document.getElementById('admin-pwd').value; const err = document.getElementById('admin-err'); if(!pwd){ err.style.display='block'; return; } // Hash the input and compare const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(pwd)); const hash = Array.from(new Uint8Array(buf)).map(b=>b.toString(16).padStart(2,'0')).join(''); if(hash === ADMIN_PWD_HASH){ _adminAuthed = true; err.style.display = 'none'; document.getElementById('admin-login').style.display = 'none'; document.getElementById('admin-workspace').style.display = 'flex'; _renderAdminList(); _loadDrawingsFromStorage(); } else { err.style.display = 'block'; document.getElementById('admin-pwd').value = ''; } } // ── Admin tabs ──────────────────────────────────────────── function admTab(tab, btn){ document.querySelectorAll('.adm-tab').forEach(b => { b.style.borderBottomColor = 'transparent'; b.style.color = 'var(--t2)'; b.classList.remove('on'); }); btn.style.borderBottomColor = 'var(--t0)'; btn.style.color = 'var(--t1)'; btn.classList.add('on'); ['list','add','draw'].forEach(t => { const el = document.getElementById('adm-'+t); if(el) el.style.display = t===tab ? 'block' : 'none'; }); if(tab==='list') _renderAdminList(); if(tab==='draw') _loadDrawingsFromStorage(); } // ── Conflict list ───────────────────────────────────────── function _renderAdminList(){ const el = document.getElementById('admin-conflict-list'); if(!el) return; const data = _getConflicts(); el.innerHTML = data.map(d => { const col = ['#4d8cf5','#f0a832','#e8445a'][d.i] || '#4d8cf5'; return `
${d.name}
${d.lat.toFixed(1)}, ${d.lng.toFixed(1)} · ${d.since||''}
`; }).join(''); } // ── Edit conflict ───────────────────────────────────────── function adminEditConflict(id){ const data = _getConflicts(); const d = data.find(x => x.id === id); if(!d) return; _editingId = id; document.getElementById('ae-id').value = id; document.getElementById('ae-name').value = d.name; document.getElementById('ae-lat').value = d.lat; document.getElementById('ae-lng').value = d.lng; document.getElementById('ae-intensity').value = d.i; document.getElementById('ae-since').value = d.since || ''; document.getElementById('ae-parties').value = d.parties || ''; document.getElementById('ae-deaths').value = d.deaths || ''; document.getElementById('ae-summary').value = d.summary || ''; document.getElementById('ae-delete-btn').style.display = 'block'; // Fly to conflict on map // Switch to add/edit tab admTab('add', document.getElementById('atab-add')); } // ── New conflict ────────────────────────────────────────── function adminNewConflict(){ _editingId = null; ['ae-id','ae-name','ae-lat','ae-lng','ae-since','ae-parties','ae-deaths','ae-summary'] .forEach(id => { const el=document.getElementById(id); if(el) el.value=''; }); document.getElementById('ae-intensity').value = '1'; document.getElementById('ae-delete-btn').style.display = 'none'; admTab('add', document.getElementById('atab-add')); } // ── Save conflict ───────────────────────────────────────── function adminSaveConflict(){ const name = document.getElementById('ae-name').value.trim(); const lat = parseFloat(document.getElementById('ae-lat').value); const lng = parseFloat(document.getElementById('ae-lng').value); const intensity = parseInt(document.getElementById('ae-intensity').value); const since = document.getElementById('ae-since').value.trim(); const parties = document.getElementById('ae-parties').value.trim(); const deaths = document.getElementById('ae-deaths').value.trim(); const summary = document.getElementById('ae-summary').value.trim(); if(!name || isNaN(lat) || isNaN(lng)){ _showAEMsg('Nombre, latitud y longitud son obligatorios.', 'var(--red)'); return; } if(lat < -90||lat > 90||lng < -180||lng > 180){ _showAEMsg('Coordenadas fuera de rango.', 'var(--red)'); return; } const data = _getConflicts(); const id = _editingId || 'custom_' + Date.now(); const entry = {id, name, lat, lng, i:intensity, since, parties, deaths, summary, cat:'general', _custom:true}; if(_editingId){ const idx = data.findIndex(x => x.id === _editingId); if(idx >= 0) data[idx] = entry; } else { data.push(entry); } _saveConflicts(data).then(()=>{}); // async shared storage _conflicts = data; _drawConflicts(); _showAEMsg('✓ Guardado (compartido)', 'var(--grn)'); setTimeout(() => admTab('list', document.getElementById('atab-list')), 800); } // ── Delete conflict ─────────────────────────────────────── function adminDeleteConflict(){ if(!_editingId) return; if(!confirm('¿Eliminar este conflicto?')) return; const data = _getConflicts().filter(x => x.id !== _editingId); _saveConflicts(data); // async _conflicts = data; _drawConflicts(); _editingId = null; admTab('list', document.getElementById('atab-list')); } // ── Reset to defaults ───────────────────────────────────── function adminResetToDefault(){ if(!confirm('¿Restaurar el dataset original? Se perderán todos los cambios.')) return; localStorage.removeItem(SK_CONFLICTS); try{ window.storage.delete('map_conflicts', true); }catch(e){} _conflicts = _getConflicts(); _drawConflicts(); _renderAdminList(); } // ── Pick coordinates on map ─────────────────────────────── function adminPickOnMap(){ alert('El mapa ha sido desactivado.'); return; _pickingCoord = true; const btn = document.getElementById('pick-map-btn'); btn.textContent = '🎯 Clic en el mapa...'; btn.style.background = 'rgba(232,68,90,.1)'; btn.style.borderColor = 'var(--red)'; const handler = e => { document.getElementById('ae-lat').value = e.latlng.lat.toFixed(4); document.getElementById('ae-lng').value = e.latlng.lng.toFixed(4); btn.textContent = `📍 ${e.latlng.lat.toFixed(3)}, ${e.latlng.lng.toFixed(3)}`; btn.style.background = 'rgba(45,212,160,.1)'; btn.style.borderColor = 'var(--grn)'; btn.style.color = 'var(--grn)'; _pickingCoord = false; // Revert after 3s setTimeout(() => { btn.textContent = '📍 Clicar en el mapa para seleccionar coordenadas'; btn.style.background = ''; btn.style.borderColor = 'var(--b0)'; btn.style.color = 'var(--t0)'; }, 3000); }; } // ── DRAW MODE ───────────────────────────────────────────── function setDrawColor(el, col){ _drawColor = col; document.querySelectorAll('.draw-col-btn').forEach(b => b.style.border = '2px solid transparent'); el.style.border = '2px solid #fff'; } // ── GitHub — shared storage via repo file ───────────────── // Setup: github.com → Settings → Developer settings → Personal access tokens // → Fine-grained token → repo: lokiff/worldmonitor → permisos: Contents (read+write) // GitHub config moved to Cloudflare Worker environment variables async function _getConflictsAsync(){ // 1. Worker GET /conflicts — sin auth, datos en GitHub via Worker try{ const r = await fetch(WORKER_CONFLICTS_GET + '?t=' + Math.floor(Date.now()/60000), { signal: AbortSignal.timeout(5000) }); if(r.ok){ const data = await r.json(); if(Array.isArray(data) && data.length){ localStorage.setItem(SK_CONFLICTS, JSON.stringify(data)); return data; } } }catch(e){} // 2. localStorage const raw = localStorage.getItem(SK_CONFLICTS); if(raw){ try{ const d=JSON.parse(raw); if(d.length) return d; }catch(e){} } // 3. Hardcoded defaults return []; } function _getConflicts(){ const raw = localStorage.getItem(SK_CONFLICTS); if(raw) try{ return JSON.parse(raw); }catch(e){} return []; } async function _saveConflicts(data){ localStorage.setItem(SK_CONFLICTS, JSON.stringify(data)); if(!ADMIN_SECRET){ console.warn('ADMIN_SECRET not set — saved locally only'); return true; } try{ const res = await fetch(WORKER_CONFLICTS_PUT, { method: 'PUT', headers:{ 'Content-Type': 'application/json', 'X-Admin-Secret': ADMIN_SECRET, }, body: JSON.stringify(data), signal: AbortSignal.timeout(12000), }); if(res.ok){ console.log('✓ conflicts.json publicado via Worker'); return true; } const err = await res.json().catch(() => ({})); console.error('Worker PUT error:', res.status, err); _showSyncStatus('✗ Error '+res.status+': '+(err.error||''), 'var(--red)'); return false; }catch(e){ console.error('Worker exception:', e); _showSyncStatus('✗ Error de red: '+e.message, 'var(--red)'); return false; } } async function adminPublish() { const btn=document.getElementById('admin-publish-btn'); const resetBtn=()=>{ if(btn){btn.disabled=false;btn.textContent='↑ PUBLISH TO GITHUB';} }; if(btn){btn.disabled=true;btn.textContent='Publishing...';} if(!GH_TOKEN){ _showSyncStatus('⚠ GH_TOKEN not set','var(--amb)'); resetBtn(); return; } try { const data=_getConflicts(); const ok=await _saveConflicts(data); _showSyncStatus(ok?'✓ Published — '+data.length+' conflicts':'✗ Error — check F12','var(--grn)'); } catch(e) { _showSyncStatus('✗ '+e.message,'var(--red)'); } finally { resetBtn(); } } function _showSyncStatus(msg,col) { const el=document.getElementById('admin-sync-status'); if(!el) return; el.textContent=msg; el.style.color=col||'var(--grn)'; setTimeout(()=>{ if(el.textContent===msg) el.textContent=''; },5000); } function openAdminPanel() { openPanel('admin'); } function adminLogin() { const pwd=document.getElementById('admin-pwd')?.value||''; const target='f7e29ca28134088a79623d800bf1204d14032dd0c445bf569e91fcdd23f1c4d4'; crypto.subtle.digest('SHA-256',new TextEncoder().encode(pwd)).then(buf=>{ const hex=[...new Uint8Array(buf)].map(b=>b.toString(16).padStart(2,'0')).join(''); if(hex===target){ document.getElementById('admin-login').style.display='none'; document.getElementById('admin-workspace').style.display='flex'; } else { const err=document.getElementById('admin-err'); if(err) err.style.display='block'; setTimeout(()=>{if(err) err.style.display='none';},2000); } }); } // ════════════════════════════════════════════════════════════ // MODULE: EVENTS MAP (MapLibre) // ════════════════════════════════════════════════════════════ let _map = null, _mapMarkers = [], _mapMode = 'conflict', _mapHidden = new Set(); function initMap() { if(_map) return; const el = document.getElementById('map-el'); if(!el) return; _map = new maplibregl.Map({ container: el, style: { version:8, sources:{carto:{type:'raster', tiles:['https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png', 'https://b.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png'], tileSize:256, attribution:'© CARTO © OSM'}}, layers:[{id:'carto',type:'raster',source:'carto'}] }, center:[20,15], zoom:2, }); _map.addControl(new maplibregl.NavigationControl(),'bottom-right'); _map.on('load', ()=>{ drawMapEvents(); buildMapLegend(); buildMapRegions(); }); } function drawMapEvents() { if(!_map) return; _mapMarkers.forEach(m=>m.remove()); _mapMarkers=[]; const items = _mapMode==='conflict' ? EVENT_CONFLICTS : EVENT_TYPES; const colorKey = _mapMode==='conflict' ? 'conflict' : 'type'; const visible = MAP_EVENTS.filter(e=>!_mapHidden.has(_mapMode==='conflict'?e.conflict:e.type)); document.getElementById('st-events').textContent = MAP_EVENTS.length; document.getElementById('st-conflicts').textContent = Object.keys(EVENT_CONFLICTS).length; document.getElementById('st-hidden').textContent = MAP_EVENTS.length - visible.length; visible.forEach(evt=>{ const color = items[evt[colorKey]]||'#ffffff'; const marker = new maplibregl.Marker({color, scale:0.65}) .setLngLat([evt.lng,evt.lat]) .setPopup(new maplibregl.Popup({closeButton:true,maxWidth:'300px',className:'dark-popup'}) .setHTML(`
${evt.type}
${evt.conflict} · ${evt.date}
${evt.location}
${evt.description}
${evt.casualties?`
⚠ ${evt.casualties}
`:''}
`)) .addTo(_map); const el=marker.getElement(); el.style.cursor='pointer'; el.style.transition='filter .15s'; el.addEventListener('mouseenter',()=>{el.style.filter=`drop-shadow(0 0 8px ${color})`;}); el.addEventListener('mouseleave',()=>{el.style.filter='none';}); _mapMarkers.push(marker); }); } function buildMapLegend() { const items = _mapMode==='conflict'?EVENT_CONFLICTS:EVENT_TYPES; const colorKey = _mapMode==='conflict'?'conflict':'type'; const el = document.getElementById('map-legend-items'); if(!el) return; el.innerHTML = Object.entries(items).map(([key,col])=>{ const count = MAP_EVENTS.filter(e=>e[colorKey]===key).length; const hidden = _mapHidden.has(key); return `
${key} ${count}
`; }).join(''); } function buildMapRegions() { const el=document.getElementById('map-regions'); if(!el) return; el.innerHTML = MAP_REGIONS.map(r=>``).join(''); } function mapFly(center, zoom) { _map?.flyTo({center, zoom, duration:1500}); } function toggleMapFilter(key) { if(_mapHidden.has(key)) _mapHidden.delete(key); else _mapHidden.add(key); drawMapEvents(); buildMapLegend(); } function setMapMode(mode, btn) { _mapMode = mode; _mapHidden.clear(); document.querySelectorAll('#map-filter-modes .tb-btn').forEach(b=>b.classList.remove('on')); btn.classList.add('on'); drawMapEvents(); buildMapLegend(); } // ════════════════════════════════════════════════════════════ // MODULE: SATELLITE IMAGERY // ════════════════════════════════════════════════════════════ let _satFilter = null; let _satDetailMap = null; const SAT_STATUS = { critical: {bg:'rgba(255,32,32,.2)',text:'#ff2020',label:'CRITICAL'}, intense: {bg:'rgba(255,140,0,.2)',text:'#ff8c00',label:'INTENSE'}, escalating:{bg:'rgba(255,204,0,.15)',text:'#ffcc00',label:'ESCALATING'}, active: {bg:'rgba(0,224,64,.12)',text:'#00e040',label:'ACTIVE'}, }; function renderSatGrid() { const grid = document.getElementById('sat-grid'); if(!grid) return; const zones = _satFilter ? SAT_ZONES.filter(z=>z.status===_satFilter) : SAT_ZONES; grid.innerHTML = zones.map(z=>{ const st = SAT_STATUS[z.status]||SAT_STATUS.active; const tileZoom = Math.max(5,z.zoom-2); const n = Math.pow(2,tileZoom); const tx = Math.floor(((z.lon+180)/360)*n); const latR = z.lat*Math.PI/180; const ty = Math.floor((1-Math.log(Math.tan(latR)+1/Math.cos(latR))/Math.PI)/2*n); const tileUrl = `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/${tileZoom}/${ty}/${tx}`; return `
${st.label}
${z.name}
${z.region} · ${z.conflict}
${z.lat.toFixed(2)}°, ${z.lon.toFixed(2)}° ↗ EXPAND
`; }).join(''); } function satFilter(status, btn) { _satFilter = status; document.querySelectorAll('#sat-filter-row .fbtn').forEach(b=>b.classList.remove('on')); btn.classList.add('on'); renderSatGrid(); } function openSatDetail(id) { const z = SAT_ZONES.find(x=>x.id===id); if(!z) return; const detail = document.getElementById('sat-detail'); document.getElementById('sat-detail-dot').style.background = z.color; document.getElementById('sat-detail-name').textContent = z.name; document.getElementById('sat-detail-sub').textContent = `${z.conflict} · ${z.region} · ${z.lat.toFixed(3)}°, ${z.lon.toFixed(3)}°`; document.getElementById('sat-detail-desc').textContent = z.description; detail.classList.add('on'); // Init detail map setTimeout(()=>{ const container = document.getElementById('sat-detail-map-el'); if(_satDetailMap) { _satDetailMap.remove(); _satDetailMap=null; } _satDetailMap = new maplibregl.Map({ container, style:{version:8,sources:{esri:{type:'raster', tiles:['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'], tileSize:256}}, layers:[{id:'esri',type:'raster',source:'esri',paint:{'raster-saturation':-1,'raster-contrast':.2}}]}, center:[z.lon,z.lat], zoom:z.zoom+1, }); _satDetailMap.addControl(new maplibregl.NavigationControl(),'bottom-right'); const el=document.createElement('div'); el.style.cssText=`width:16px;height:16px;border-radius:50%;background:${z.color};border:2px solid rgba(0,0,0,.5);box-shadow:0 0 10px ${z.color}88;`; new maplibregl.Marker({element:el}).setLngLat([z.lon,z.lat]).addTo(_satDetailMap); setTimeout(()=>_satDetailMap.resize(),100); },50); } function closeSatDetail() { document.getElementById('sat-detail').classList.remove('on'); if(_satDetailMap){_satDetailMap.remove();_satDetailMap=null;} } // ════════════════════════════════════════════════════════════ // MODULE: DEATH TRACKER // ════════════════════════════════════════════════════════════ let _deathsFilter = 'all'; function initDeaths() { const today = _todayStr(); const total = DEATH_CONFLICTS.reduce((s,c)=>s+_getDailyDeaths(c,today),0); const escalating = DEATH_CONFLICTS.filter(c=>c.trend==='escalating').length; document.getElementById('dt-today').textContent = total.toLocaleString(); document.getElementById('dt-zones').textContent = DEATH_CONFLICTS.length; document.getElementById('dt-escalating').textContent = escalating; renderDeathsList(); } function renderDeathsList() { const today = _todayStr(); let data = DEATH_CONFLICTS.map(c=>({...c, todayDeaths:_getDailyDeaths(c,today)})) .sort((a,b)=>b.todayDeaths-a.todayDeaths); if(_deathsFilter==='major_war') data=data.filter(c=>c.severity==='major_war'); else if(_deathsFilter==='war') data=data.filter(c=>c.severity==='war'); else if(_deathsFilter==='conflict') data=data.filter(c=>c.severity==='conflict'); else if(_deathsFilter==='escalating') data=data.filter(c=>c.trend==='escalating'); const sevColors = {major_war:'var(--red)',war:'var(--amb)',conflict:'#ffcc00'}; const trendIcons = {escalating:'↑',stable:'→','de-escalating':'↓'}; const trendCols = {escalating:'var(--red)',stable:'var(--t2)','de-escalating':'var(--grn)'}; // Sparklines - 7 days const spark = (c)=>{ const days=Array.from({length:7},(_,i)=>_getDailyDeaths(c,new Date(Date.now()-i*86400000).toISOString().slice(0,10))).reverse(); const max=Math.max(...days,1); return days.map(v=>`
`).join(''); }; document.getElementById('deaths-list').innerHTML = data.map(c=>`
${c.todayDeaths}
today
${c.name} ${c.severity.replace('_',' ').toUpperCase()} ${trendIcons[c.trend]||'→'} ${c.trend}
${c.country} · ${c.parties.join(' vs ')}
${spark(c)}
${c.description}
TODAY
${c.todayDeaths}
AVG/DAY
${c.avgDailyDeaths}
TOTAL EST.
${(c.cumulativeDeaths/1000).toFixed(0)}K+
SINCE
${c.startYear}
`).join(''); } function toggleDcExpand(id) { const el=document.getElementById('dc-exp-'+id); if(!el) return; el.classList.toggle('on'); } function deathsFilter(f, btn) { _deathsFilter=f; document.querySelectorAll('#deaths-filter .fbtn').forEach(b=>b.classList.remove('on')); btn.classList.add('on'); renderDeathsList(); } // ════════════════════════════════════════════════════════════ // MODULE: MARKETS // ════════════════════════════════════════════════════════════ const COMMODITIES = [ {symbol:'GC=F',name:'Gold',cat:'metal',unit:'/oz'}, {symbol:'SI=F',name:'Silver',cat:'metal',unit:'/oz'}, {symbol:'HG=F',name:'Copper',cat:'metal',unit:'/lb'}, {symbol:'CL=F',name:'WTI Crude',cat:'energy',unit:'/bbl'}, {symbol:'BZ=F',name:'Brent Crude',cat:'energy',unit:'/bbl'}, {symbol:'NG=F',name:'Natural Gas',cat:'energy',unit:'/MMBtu'}, {symbol:'ZW=F',name:'Wheat',cat:'agri',unit:'/bu'}, {symbol:'ZC=F',name:'Corn',cat:'agri',unit:'/bu'}, {symbol:'KC=F',name:'Coffee',cat:'agri',unit:'/lb'}, ]; const INDICES = [ {symbol:'%5EGSPC',name:'S&P 500',cat:'idx'}, {symbol:'%5EIXIC',name:'NASDAQ',cat:'idx'}, {symbol:'%5EDAX',name:'DAX',cat:'idx'}, {symbol:'%5EN225',name:'NIKKEI',cat:'idx'}, ]; const CRYPTO = [ {symbol:'BTC-USD',name:'Bitcoin',cat:'crypto'}, {symbol:'ETH-USD',name:'Ethereum',cat:'crypto'}, {symbol:'SOL-USD',name:'Solana',cat:'crypto'}, ]; const FX = [ {symbol:'EURUSD=X',name:'EUR/USD',cat:'fx'}, {symbol:'GBPUSD=X',name:'GBP/USD',cat:'fx'}, {symbol:'JPY=X',name:'USD/JPY',cat:'fx'}, {symbol:'CNY=X',name:'USD/CNY',cat:'fx'}, ]; let _mktCountdown = 60; let _mktTimer = null; async function fetchYahoo(symbol) { const url = `https://query1.finance.yahoo.com/v8/finance/chart/${symbol}?interval=1d&range=5d`; try { const r = await fetch('/api/proxy?url='+encodeURIComponent(url), {signal:AbortSignal.timeout(8000)}); if(!r.ok) throw 0; const j = await r.json(); const meta = j?.chart?.result?.[0]?.meta; if(!meta) throw 0; const price = meta.regularMarketPrice; const prev = meta.chartPreviousClose||meta.previousClose; const change = price&&prev ? price-prev : null; const changePct = change&&prev ? change/prev*100 : null; return {price,change,changePct}; } catch { return null; } } function fmtPrice(p, sym) { if(p==null) return '—'; const cents = ['ZC=F','ZW=F','ZS=F','KC=F','SB=F','CT=F','CC=F']; if(cents.includes(sym)) return p.toFixed(2)+'¢'; if(p>10000) return '$'+p.toLocaleString('en',{maximumFractionDigits:0}); if(p>100) return '$'+p.toFixed(2); return '$'+p.toFixed(4); } function fmtChg(v, pct) { if(v==null) return '—'; const prefix=v>0?'+':''; const cls=v>0?'mkt-up':v<0?'mkt-dn':'mkt-flat'; const badge=v>0?'mkt-badge-up':'mkt-badge-dn'; return `${prefix}${v.toFixed(2)} ${prefix}${pct?.toFixed(2)||'0.00'}%`; } async function refreshMarkets(manual=false) { if(manual) { clearInterval(_mktTimer); _mktCountdown=60; } const catStyles = { metal:'cat-metal',energy:'cat-energy',agri:'cat-agri',crypto:'cat-crypto',idx:'',fx:'cat-fx' }; const mkRow = (sym,name,cat,data,unit='')=>` ${name} ${cat.toUpperCase()} ${fmtPrice(data?.price,sym)}${unit?`
${unit}
`:''} ${data?fmtChg(data.change,data.changePct):''} `; const sets = [ {items:COMMODITIES,tbody:'mkt-commodities'}, {items:INDICES,tbody:'mkt-indices'}, {items:CRYPTO,tbody:'mkt-crypto'}, {items:FX,tbody:'mkt-fx'}, ]; // Show loading skeletons sets.forEach(({tbody})=>{ const el=document.getElementById(tbody); if(el) el.innerHTML=`Loading...`; }); // Fetch all in parallel for(const {items,tbody} of sets) { const results = await Promise.all(items.map(i=>fetchYahoo(i.symbol))); const el = document.getElementById(tbody); if(!el) continue; el.innerHTML = items.map((item,i)=>mkRow(item.symbol,item.name,item.cat,results[i],item.unit||'')).join(''); } // Restart countdown _mktCountdown = 60; clearInterval(_mktTimer); _mktTimer = setInterval(()=>{ _mktCountdown--; const cd=document.getElementById('mkt-countdown'); if(cd) cd.textContent=_mktCountdown+'s'; if(_mktCountdown<=0) refreshMarkets(); },1000); } // ════════════════════════════════════════════════════════════ // MODULE: TELEGRAM OSINT // ════════════════════════════════════════════════════════════ let _tgPosts = []; let _tgRegion = null; let _tgChannel = null; let _tgSeenIds = new Set(); async function fetchTGChannel(ch) { const url = `https://t.me/s/${ch.name}`; try { const r = await fetch(WORKER+'/telegram?ch='+encodeURIComponent(ch.name),{signal:AbortSignal.timeout(8000)}); if(!r.ok) return []; const d = await r.json(); return (d.items||[]).map(item=>({ id: ch.name+'_'+item.link, channelName: ch.name, channelLabel: ch.label, text: item.title||item.desc||'', timestamp: new Date(item.pubDate||Date.now()), link: item.link||`https://t.me/${ch.name}`, region: ch.region, color: ch.color, bgColor: ch.bgColor, isNew: !_tgSeenIds.has(ch.name+'_'+item.link), })); } catch { return []; } } async function refreshTelegram() { colStatus('tg-count','loading'); const all = []; await Promise.allSettled(TG_CHANNELS.map(async ch=>{ const posts = await fetchTGChannel(ch); posts.forEach(p=>all.push(p)); })); all.sort((a,b)=>b.timestamp-a.timestamp); _tgPosts = all; all.forEach(p=>_tgSeenIds.add(p.id)); buildTgFilterBars(); renderTgFeed(); const el=document.getElementById('tg-count'); if(el) el.textContent = all.length; } function buildTgFilterBars() { const regions = [...new Set(TG_CHANNELS.map(c=>c.region))]; const rbar = document.getElementById('tg-region-bar'); if(rbar) rbar.innerHTML = `` + regions.map(r=>``).join(''); const cbar = document.getElementById('tg-channel-bar'); const chsToShow = _tgRegion ? TG_CHANNELS.filter(c=>c.region===_tgRegion) : TG_CHANNELS; if(cbar) cbar.innerHTML = chsToShow.map(ch=>``).join(''); } function tgFilter(region, channel, btn) { _tgRegion = region==='null'?null:region; _tgChannel = channel==='null'?null:channel; document.querySelectorAll('#tg-region-bar .fbtn,#tg-channel-bar .fbtn').forEach(b=>b.classList.remove('on')); btn.classList.add('on'); buildTgFilterBars(); renderTgFeed(); } function renderTgFeed() { const feed=document.getElementById('tg-feed'); if(!feed) return; let posts = _tgPosts; if(_tgChannel) posts=posts.filter(p=>p.channelName===_tgChannel); else if(_tgRegion) posts=posts.filter(p=>p.region===_tgRegion); if(!posts.length){ feed.innerHTML='
No posts · Click REFRESH to load
'; return; } feed.innerHTML = posts.slice(0,80).map(p=>{ const rel = Math.floor((Date.now()-p.timestamp)/60000); const time = rel<60?rel+'m ago':Math.floor(rel/60)+'h ago'; return `
${p.channelLabel} ${p.region} ${time} ${p.isNew?'NEW':''}
${p.text.slice(0,280)}${p.text.length>280?'…':''}
↗ source
`; }).join(''); } // ════════════════════════════════════════════════════════════ // MODULE: AIRSPACE / NOTAM // ════════════════════════════════════════════════════════════ let _notamRegion = 'Global'; let _notamType = 'All'; let _expandedNotam = null; function initAirspace() { _rawNotams = _generateNotams(); renderNotams(); buildNotamTicker(); } function notamFilter(what, val, btn) { if(what==='region') { _notamRegion=val; document.querySelectorAll('#notam-filter-bar .fbtn').slice(0,6).forEach(b=>b.classList.remove('on')); } else { _notamType=val; } btn.classList.add('on'); renderNotams(); } function renderNotams() { const filtered = _rawNotams.filter(n=>{ if(_notamRegion!=='Global' && n.region!==_notamRegion) return false; if(_notamType!=='All' && n.type!==_notamType) return false; return true; }); const critCount = _rawNotams.filter(n=>n.severity==='critical'||n.type==='CONFLICT').length; const alertBar = document.getElementById('notam-alert-bar'); const badge = document.getElementById('notam-level-badge'); const summary = document.getElementById('notam-summary'); if(alertBar&&badge&&summary) { const isCrit = critCount>0; alertBar.style.background = isCrit?'rgba(255,32,32,.08)':'rgba(255,140,0,.05)'; alertBar.style.borderBottomColor = isCrit?'rgba(255,32,32,.3)':'rgba(255,140,0,.2)'; badge.textContent = isCrit?'● CRITICAL':'● ELEVATED'; badge.style.color = isCrit?'var(--red)':'var(--amb)'; summary.textContent = `${critCount} conflict zones · ${_rawNotams.filter(n=>n.type==='MIL').length} mil exercises · ${_rawNotams.length} active NOTAMs`; } const tc={CONFLICT:'var(--red)',MIL:'var(--amb)',TFR:'#eab308',LAUNCH:'var(--blu)',VIP:'#a855f7'}; const el = document.getElementById('notam-list'); if(!el) return; el.innerHTML = filtered.map(n=>`
${n.type} ${n.notamId} ${n.isNew?'NEW':''} ${n.severity==='critical'?'CRITICAL':''}
${n.location}
📍 ${n.fir} ↕ ${n.lowerAlt}–${n.upperAlt} ${n.region}
${n.description.slice(0,120)}...
${n.description}
${(n.keywords||[]).map(k=>`${k}`).join('')} ${n.isConflictZone?'CONFLICT ZONE':''}
`).join(''); } function toggleNotam(id) { _expandedNotam = _expandedNotam===id?null:id; renderNotams(); } function buildNotamTicker() { const items = _rawNotams.slice(0,12); const html = items.map(n=>`▸ [${n.notamId}] ${n.type} — ${n.location}: ${n.description.slice(0,60)}...`).join(''); const inner = document.getElementById('ntick-inner'); if(inner) inner.innerHTML = html+html; } // ════════════════════════════════════════════════════════════ // MODULE SWITCHING // ════════════════════════════════════════════════════════════ let _currentModule = 'feed'; const MODULE_INITS = { map: ()=>{ if(!_map) initMap(); }, satellite: ()=>{ if(!document.querySelector('.sat-card')) renderSatGrid(); }, deaths: ()=>{ if(!document.querySelector('.dc-item')) initDeaths(); }, markets: ()=>{ if(!document.querySelector('.mkt-table td')) refreshMarkets(); }, telegram: ()=>{ if(!_tgPosts.length) refreshTelegram(); }, airspace: ()=>{ if(!_rawNotams.length) initAirspace(); }, }; function switchModule(name, btn) { document.querySelectorAll('.module').forEach(m=>m.classList.remove('on')); document.querySelectorAll('.mod-pill').forEach(b=>b.classList.remove('on')); const mod = document.getElementById('mod-'+name); if(mod) mod.classList.add('on'); btn.classList.add('on'); _currentModule = name; if(MODULE_INITS[name]) MODULE_INITS[name](); } // ════════════════════════════════════════════════════════════ // PANEL / SETTINGS // ════════════════════════════════════════════════════════════ function openPanel(id) { document.getElementById('panel-overlay').style.display='block'; document.querySelectorAll('.side-panel').forEach(p=>p.classList.remove('open')); const p=document.getElementById('panel-'+id); if(p) p.classList.add('open'); } function closePanel() { document.getElementById('panel-overlay').style.display='none'; document.querySelectorAll('.side-panel').forEach(p=>p.classList.remove('open')); } function toggleTheme() { document.body.classList.toggle('light'); } // ════════════════════════════════════════════════════════════ // CLOCK & PREFS // ════════════════════════════════════════════════════════════ function startClock(){ const el = document.getElementById('clock'); setInterval(()=>{ const n=new Date(); if(el) el.textContent = String(n.getUTCHours()).padStart(2,'0')+':'+ String(n.getUTCMinutes()).padStart(2,'0')+':'+ String(n.getUTCSeconds()).padStart(2,'0')+' UTC'; },1000); } // ═══════════════════════════════════════════════════════════ // INIT // ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════ // ADMIN — Map editor (password protected) // ═══════════════════════════════════════════════════════════ // Admin password — stored as SHA-256 hash // Default password: "conflicttracker2025" // To change: run btoa(await crypto.subtle.digest('SHA-256', new TextEncoder().encode('newpassword'))) const ADMIN_PWD_HASH = 'f7e29ca28134088a79623d800bf1204d14032dd0c445bf569e91fcdd23f1c4d4'; let _drawLayers = []; // saved drawing layers // ── Admin: Publish & Sync ───────────────────────────────── async function adminPublish(){ const btn = document.getElementById('admin-publish-btn'); const resetBtn = () => { if(btn){ btn.disabled=false; btn.innerHTML='↑ Publicar cambios'; } }; if(btn){ btn.disabled=true; btn.innerHTML='Publicando...'; } if(!ADMIN_SECRET){ document.getElementById('admin-jsonbin-setup').style.display = 'block'; _showSyncStatus('⚠ ADMIN_SECRET no configurado', 'var(--amb)'); resetBtn(); return; } try{ const data = _getConflicts(); const ok = await _saveConflicts(data); _showSyncStatus( ok ? '✓ Publicado — '+data.length+' conflictos en GitHub' : '✗ Error — revisa la consola (F12)', ok ? 'var(--grn)' : 'var(--red)' ); }catch(e){ console.error('adminPublish error:', e); _showSyncStatus('✗ Error: '+e.message, 'var(--red)'); } finally { resetBtn(); } } function _showSyncStatus(msg, col){ col = col || 'var(--grn)'; const el = document.getElementById('admin-sync-status'); if(!el) return; el.textContent = msg; el.style.color = col; setTimeout(() => { if(el.textContent===msg) el.textContent=''; }, 5000); } async function _pollRemoteConflicts(){ if(_adminAuthed) return; try{ const r = await fetch(WORKER_CONFLICTS_GET + '?t=' + Math.floor(Date.now()/60000), { signal: AbortSignal.timeout(6000), }); if(!r.ok) return; const data = await r.json(); if(!Array.isArray(data)) return; if(!Array.isArray(data) || !data.length) return; const current = JSON.stringify(_conflicts); if(JSON.stringify(data) !== current){ _conflicts = data; localStorage.setItem(SK_CONFLICTS, JSON.stringify(data)); _drawConflicts(); } }catch(e){} } // Storage keys const SK_CONFLICTS = 'ct_map_conflicts'; const SK_DRAWINGS = 'ct_map_drawings'; // ── Open admin panel ────────────────────────────────────── function openAdminPanel(){ openPanel('admin'); if(_adminAuthed){ document.getElementById('admin-login').style.display = 'none'; document.getElementById('admin-workspace').style.display = 'flex'; _renderAdminList(); _loadDrawingsFromStorage(); } } // ── Login ───────────────────────────────────────────────── async function adminLogin(){ const pwd = document.getElementById('admin-pwd').value; const err = document.getElementById('admin-err'); if(!pwd){ err.style.display='block'; return; } // Hash the input and compare const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(pwd)); const hash = Array.from(new Uint8Array(buf)).map(b=>b.toString(16).padStart(2,'0')).join(''); if(hash === ADMIN_PWD_HASH){ _adminAuthed = true; err.style.display = 'none'; document.getElementById('admin-login').style.display = 'none'; document.getElementById('admin-workspace').style.display = 'flex'; _renderAdminList(); _loadDrawingsFromStorage(); } else { err.style.display = 'block'; document.getElementById('admin-pwd').value = ''; } } // ── Admin tabs ──────────────────────────────────────────── function admTab(tab, btn){ document.querySelectorAll('.adm-tab').forEach(b => { b.style.borderBottomColor = 'transparent'; b.style.color = 'var(--t2)'; b.classList.remove('on'); }); btn.style.borderBottomColor = 'var(--t0)'; btn.style.color = 'var(--t1)'; btn.classList.add('on'); ['list','add','draw'].forEach(t => { const el = document.getElementById('adm-'+t); if(el) el.style.display = t===tab ? 'block' : 'none'; }); if(tab==='list') _renderAdminList(); if(tab==='draw') _loadDrawingsFromStorage(); } // ── Conflict list ───────────────────────────────────────── function _renderAdminList(){ const el = document.getElementById('admin-conflict-list'); if(!el) return; const data = _getConflicts(); el.innerHTML = data.map(d => { const col = ['#4d8cf5','#f0a832','#e8445a'][d.i] || '#4d8cf5'; return `
${d.name}
${d.lat.toFixed(1)}, ${d.lng.toFixed(1)} · ${d.since||''}
`; }).join(''); } // ── Edit conflict ───────────────────────────────────────── function adminEditConflict(id){ const data = _getConflicts(); const d = data.find(x => x.id === id); if(!d) return; _editingId = id; document.getElementById('ae-id').value = id; document.getElementById('ae-name').value = d.name; document.getElementById('ae-lat').value = d.lat; document.getElementById('ae-lng').value = d.lng; document.getElementById('ae-intensity').value = d.i; document.getElementById('ae-since').value = d.since || ''; document.getElementById('ae-parties').value = d.parties || ''; document.getElementById('ae-deaths').value = d.deaths || ''; document.getElementById('ae-summary').value = d.summary || ''; document.getElementById('ae-delete-btn').style.display = 'block'; // Fly to conflict on map // Switch to add/edit tab admTab('add', document.getElementById('atab-add')); } // ── New conflict ────────────────────────────────────────── function adminNewConflict(){ _editingId = null; ['ae-id','ae-name','ae-lat','ae-lng','ae-since','ae-parties','ae-deaths','ae-summary'] .forEach(id => { const el=document.getElementById(id); if(el) el.value=''; }); document.getElementById('ae-intensity').value = '1'; document.getElementById('ae-delete-btn').style.display = 'none'; admTab('add', document.getElementById('atab-add')); } // ── Save conflict ───────────────────────────────────────── function adminSaveConflict(){ const name = document.getElementById('ae-name').value.trim(); const lat = parseFloat(document.getElementById('ae-lat').value); const lng = parseFloat(document.getElementById('ae-lng').value); const intensity = parseInt(document.getElementById('ae-intensity').value); const since = document.getElementById('ae-since').value.trim(); const parties = document.getElementById('ae-parties').value.trim(); const deaths = document.getElementById('ae-deaths').value.trim(); const summary = document.getElementById('ae-summary').value.trim(); if(!name || isNaN(lat) || isNaN(lng)){ _showAEMsg('Nombre, latitud y longitud son obligatorios.', 'var(--red)'); return; } if(lat < -90||lat > 90||lng < -180||lng > 180){ _showAEMsg('Coordenadas fuera de rango.', 'var(--red)'); return; } const data = _getConflicts(); const id = _editingId || 'custom_' + Date.now(); const entry = {id, name, lat, lng, i:intensity, since, parties, deaths, summary, cat:'general', _custom:true}; if(_editingId){ const idx = data.findIndex(x => x.id === _editingId); if(idx >= 0) data[idx] = entry; } else { data.push(entry); } _saveConflicts(data).then(()=>{}); // async shared storage _conflicts = data; _drawConflicts(); _showAEMsg('✓ Guardado (compartido)', 'var(--grn)'); setTimeout(() => admTab('list', document.getElementById('atab-list')), 800); } // ── Delete conflict ─────────────────────────────────────── function adminDeleteConflict(){ if(!_editingId) return; if(!confirm('¿Eliminar este conflicto?')) return; const data = _getConflicts().filter(x => x.id !== _editingId); _saveConflicts(data); // async _conflicts = data; _drawConflicts(); _editingId = null; admTab('list', document.getElementById('atab-list')); } // ── Reset to defaults ───────────────────────────────────── function adminResetToDefault(){ if(!confirm('¿Restaurar el dataset original? Se perderán todos los cambios.')) return; localStorage.removeItem(SK_CONFLICTS); try{ window.storage.delete('map_conflicts', true); }catch(e){} _conflicts = _getConflicts(); _drawConflicts(); _renderAdminList(); } // ── Pick coordinates on map ─────────────────────────────── function adminPickOnMap(){ alert('El mapa ha sido desactivado.'); return; _pickingCoord = true; const btn = document.getElementById('pick-map-btn'); btn.textContent = '🎯 Clic en el mapa...'; btn.style.background = 'rgba(232,68,90,.1)'; btn.style.borderColor = 'var(--red)'; const handler = e => { document.getElementById('ae-lat').value = e.latlng.lat.toFixed(4); document.getElementById('ae-lng').value = e.latlng.lng.toFixed(4); btn.textContent = `📍 ${e.latlng.lat.toFixed(3)}, ${e.latlng.lng.toFixed(3)}`; btn.style.background = 'rgba(45,212,160,.1)'; btn.style.borderColor = 'var(--grn)'; btn.style.color = 'var(--grn)'; _pickingCoord = false; // Revert after 3s setTimeout(() => { btn.textContent = '📍 Clicar en el mapa para seleccionar coordenadas'; btn.style.background = ''; btn.style.borderColor = 'var(--b0)'; btn.style.color = 'var(--t0)'; }, 3000); }; } // ── DRAW MODE ───────────────────────────────────────────── function setDrawColor(el, col){ _drawColor = col; document.querySelectorAll('.draw-col-btn').forEach(b => b.style.border = '2px solid transparent'); el.style.border = '2px solid #fff'; } // ── GitHub — shared storage via repo file ───────────────── // Setup: github.com → Settings → Developer settings → Personal access tokens // → Fine-grained token → repo: lokiff/worldmonitor → permisos: Contents (read+write) // GitHub config moved to Cloudflare Worker environment variables async function _getConflictsAsync(){ // 1. Worker GET /conflicts — sin auth, datos en GitHub via Worker try{ const r = await fetch(WORKER_CONFLICTS_GET + '?t=' + Math.floor(Date.now()/60000), { signal: AbortSignal.timeout(5000) }); if(r.ok){ const data = await r.json(); if(Array.isArray(data) && data.length){ localStorage.setItem(SK_CONFLICTS, JSON.stringify(data)); return data; } } }catch(e){} // 2. localStorage const raw = localStorage.getItem(SK_CONFLICTS); if(raw){ try{ const d=JSON.parse(raw); if(d.length) return d; }catch(e){} } // 3. Hardcoded defaults return []; } function _getConflicts(){ const raw = localStorage.getItem(SK_CONFLICTS); if(raw) try{ return JSON.parse(raw); }catch(e){} return []; } async function _saveConflicts(data){ localStorage.setItem(SK_CONFLICTS, JSON.stringify(data)); if(!ADMIN_SECRET){ console.warn('ADMIN_SECRET not set — saved locally only'); return true; } try{ const res = await fetch(WORKER_CONFLICTS_PUT, { method: 'PUT', headers:{ 'Content-Type': 'application/json', 'X-Admin-Secret': ADMIN_SECRET, }, body: JSON.stringify(data), signal: AbortSignal.timeout(12000), }); if(res.ok){ console.log('✓ conflicts.json publicado via Worker'); return true; } const err = await res.json().catch(() => ({})); console.error('Worker PUT error:', res.status, err); _showSyncStatus('✗ Error '+res.status+': '+(err.error||''), 'var(--red)'); return false; }catch(e){ console.error('Worker exception:', e); _showSyncStatus('✗ Error de red: '+e.message, 'var(--red)'); return false; } } function _showAEMsg(msg, col){ const el = document.getElementById('ae-msg'); if(!el) return; el.textContent = msg; el.style.color = col; el.style.display = 'block'; setTimeout(() => { el.style.display='none'; }, 2500); } // ═══════════════════════════════════════════════════════════ // MOBILE TAB NAVIGATION // ═══════════════════════════════════════════════════════════ function mobTab(which, btn){ // Update tab buttons document.querySelectorAll('.mob-tab').forEach(b => b.classList.remove('on')); btn.classList.add('on'); // Show correct column document.querySelectorAll('.col').forEach(c => c.classList.remove('mob-active')); const col = document.getElementById('col-' + which); if(col) col.classList.add('mob-active'); } // Keep mobile badges in sync with col badges function syncMobBadges(){ const mn = document.getElementById('ft-media'); const mo = document.getElementById('ft-osint'); const bn = document.getElementById('mbadge-news'); const bo = document.getElementById('mbadge-osint'); if(mn && bn) bn.textContent = mn.textContent || '—'; if(mo && bo) bo.textContent = mo.textContent || '—'; } function init(){ loadPrefs(); startClock(); renderSaved(); renderCustomList(); // Initial load progSet(2); setTimeout(async ()=>{ await Promise.all([loadMedia(), loadOSINT()]); setLastUpdated(); setTimeout(loadBreakingBar, 300); setTimeout(loadGDELT, 800); setTimeout(loadOREF, 1200); }, 300); // Background refresh — tab-aware, no countdown startAutoRefresh(); renderConflicts(); setInterval(loadOREF, 15000); setInterval(loadGDELT, 15*60*1000); setInterval(_pollRemoteConflicts, 5*60*1000); } document.addEventListener('DOMContentLoaded', init); // ════════════════════════════════════════════════════════════ // REFRESH ALL & INIT // ════════════════════════════════════════════════════════════ async function refreshAll(){ if(_refreshing) return; _refreshing=true; const btn=document.getElementById('refresh-btn'); if(btn){btn.classList.add('active');btn.disabled=true;} progSet(2); mediaCache={ts:0,data:[]}; osintCache={ts:0,data:[]}; gdeltCache={ts:0,data:[]}; brkLoaded=false; const _fm=document.getElementById('feed-media'); const _fo=document.getElementById('feed-osint'); // Keep existing items visible during refresh — no blanking, no flash try{ await Promise.all([loadMedia(),loadOSINT()]); setLastUpdated(); // feeds already updated in place by loadMedia/loadOSINT loadBreakingBar(); loadGDELT(); }finally{ _refreshing=false; if(btn){btn.classList.remove('active');btn.disabled=false;} } } // ═══════════════════════════════════════════════════════════ // AUTO-REFRESH — tab-aware, 5min interval, no countdown // ═══════════════════════════════════════════════════════════ function startAutoRefresh(){ const INTERVAL=5*60*1000; let lastRun=Date.now(); function schedule(){ const wait=Math.max(0,INTERVAL-(Date.now()-lastRun)); setTimeout(async()=>{ if(!document.hidden){lastRun=Date.now();await refreshAll();} schedule(); },wait); } document.addEventListener('visibilitychange',()=>{ if(!document.hidden&&Date.now()-lastRun>INTERVAL){lastRun=Date.now();refreshAll();} }); schedule(); } // ═══════════════════════════════════════════════════════════ // ITEM ACTIONS // ═══════════════════════════════════════════════════════════ function openItem(href){ window.open(href,'_blank'); markRead(href); } function markRead(href){ if(!href) return; readSet.add(href); try{ localStorage.setItem('ct_read', JSON.stringify([...readSet].slice(-500))); }catch(e){} // Visually mark document.querySelectorAll('.fitem').forEach(el => { if(el.getAttribute('onclick')?.includes(href)) el.classList.add('read'); }); } function toggleRead(btn, href){ const item = btn.closest('.fitem'); if(readSet.has(href)){ readSet.delete(href); item.classList.remove('read'); btn.classList.remove('read-on'); } else { readSet.add(href); item.classList.add('read'); btn.classList.add('read-on'); } try{ localStorage.setItem('ct_read', JSON.stringify([...readSet].slice(-500))); }catch(e){} } function saveItem(item){ if(savedItems.find(s=>s.link===item.link)) return; savedItems.unshift(item); if(savedItems.length>100) savedItems.pop(); try{ localStorage.setItem('ct_saved', JSON.stringify(savedItems)); }catch(e){} } // ═══════════════════════════════════════════════════════════ // SIDEBAR MODES // ═══════════════════════════════════════════════════════════ function setSidebar(mode, btn){ currentSidebar = mode; document.querySelectorAll('.sb-btn').forEach(b=>b.classList.remove('on')); btn.classList.add('on'); const media = document.getElementById('feed-media'); const saved = document.getElementById('feed-saved'); const fm = document.getElementById('filters-media'); if(mode==='saved'){ media.style.display='none'; if(fm) fm.style.display='none'; saved.style.display=''; renderSaved(); } else { media.style.display=''; if(fm) fm.style.display=''; saved.style.display='none'; } } function renderSaved(){ const el = document.getElementById('feed-saved'); if(!el) return; if(!savedItems.length){ el.innerHTML='
Sin guardadas.
'; return; } el.innerHTML = savedItems.map((item,i) => `
${item.title}
${item.src}
`).join(''); } // ═══════════════════════════════════════════════════════════ // FILTER FEEDS // ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════ // TWITTER — RSSHub feed (same engine as OSINT) // ═══════════════════════════════════════════════════════════ function filterFeed(f, btn, which){ if(which==='media') activeCatMedia=f; else activeCatOsint=f; btn.closest('.filters').querySelectorAll('.fbtn').forEach(b=>b.classList.remove('on')); btn.classList.add('on'); if(which==='media') renderFeed(mediaCache.data, 'feed-media'); else renderFeed(osintCache.data, 'feed-osint'); } // ═══════════════════════════════════════════════════════════ // PANELS // ═══════════════════════════════════════════════════════════ function openPanel(id){ closePanel(); activePanel = id; document.getElementById('panel-overlay').style.display = 'block'; document.getElementById('panel-'+id)?.classList.add('open'); } function closePanel(){ document.querySelectorAll('.side-panel').forEach(p=>p.classList.remove('open')); document.getElementById('panel-overlay').style.display = 'none'; activePanel = null; } // ═══════════════════════════════════════════════════════════ // OREF // ═══════════════════════════════════════════════════════════ async function loadOREF(){ try{ const r = await fetch(WORKER+'/oref', {signal:AbortSignal.timeout(5000)}); if(!r.ok) return; const d = await r.json(); const banner = document.getElementById('oref-banner'); if(d.status==='active' && d.alerts?.length){ banner.classList.add('active'); document.getElementById('oref-text').textContent = '🚨 ALERTA COHETES — ' + d.alerts.map(a=>a.data||'').flat().join(' · '); } else { banner.classList.remove('active'); } }catch(e){} } // ═══════════════════════════════════════════════════════════ // PREFS // ═══════════════════════════════════════════════════════════ function applyFont(v){ // v is the raw option value e.g. "Inter" or "'JetBrains Mono'" document.documentElement.style.setProperty('--font', v + ',sans-serif'); localStorage.setItem('ct_font', v); const s = document.getElementById('pref-font'); if(s) s.value = v; } function applySize(v){ v = parseInt(v); document.documentElement.style.fontSize = v+'px'; const sv = document.getElementById('size-val'); if(sv) sv.textContent=v+'px'; const sl = document.getElementById('pref-size'); if(sl) sl.value=v; localStorage.setItem('ct_size', v); } function applyAccent(v, dot){ document.documentElement.style.setProperty('--blue', v); document.documentElement.style.setProperty('--accent', v); localStorage.setItem('ct_accent', v); document.querySelectorAll('.cdot').forEach(d=>d.classList.remove('on')); if(dot) dot.classList.add('on'); } function toggleTheme(){ document.body.classList.toggle('light'); const isLight = document.body.classList.contains('light'); const btn = document.getElementById('theme-btn'); const tog = document.getElementById('tog-dark'); if(tog) tog.classList.toggle('on', !isLight); localStorage.setItem('ct_theme', isLight?'light':'dark'); } function resetPrefs(){ ['ct_font','ct_size','ct_accent','ct_theme'].forEach(k=>localStorage.removeItem(k)); location.reload(); } function loadPrefs(){ const font = localStorage.getItem('ct_font'); if(font) applyFont(font); const size = localStorage.getItem('ct_size'); if(size) applySize(size); const accent = localStorage.getItem('ct_accent'); if(accent){ document.documentElement.style.setProperty('--blue', accent); document.querySelectorAll('.cdot').forEach(d => { if(d.style.background===accent||d.getAttribute('onclick')?.includes(accent)){ d.classList.add('on'); } else d.classList.remove('on'); }); } if(localStorage.getItem('ct_theme')==='light'){ document.body.classList.add('light'); const tog=document.getElementById('tog-dark'); if(tog) tog.classList.remove('on'); } } // ═══════════════════════════════════════════════════════════ // CUSTOM SOURCES // ═══════════════════════════════════════════════════════════ function addSource(){ const name = document.getElementById('src-name').value.trim(); const url = document.getElementById('src-url').value.trim(); const cat = document.getElementById('src-cat').value; if(!name||!url) return; customSources.push({n:name, url, cat}); localStorage.setItem('ct_custom', JSON.stringify(customSources)); document.getElementById('src-name').value = ''; document.getElementById('src-url').value = ''; renderCustomList(); } function removeSource(i){ customSources.splice(i,1); localStorage.setItem('ct_custom', JSON.stringify(customSources)); renderCustomList(); } function renderCustomList(){ const el = document.getElementById('custom-list'); if(!el) return; if(!customSources.length){ el.innerHTML='
Sin fuentes personalizadas.
'; return; } el.innerHTML = customSources.map((s,i) => `
${s.n} ${s.cat}
`).join(''); } // ═══════════════════════════════════════════════════════════ // CLOCK // ═══════════════════════════════════════════════════════════ function startClock(){ const el = document.getElementById('clock'); setInterval(()=>{ const n=new Date(); if(el) el.textContent = String(n.getUTCHours()).padStart(2,'0')+':'+ String(n.getUTCMinutes()).padStart(2,'0')+':'+ String(n.getUTCSeconds()).padStart(2,'0')+' UTC'; },1000); } // ═══════════════════════════════════════════════════════════ // INIT // ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════ // ADMIN — Map editor (password protected) // ═══════════════════════════════════════════════════════════ // Admin password — stored as SHA-256 hash // Default password: "conflicttracker2025" // To change: run btoa(await crypto.subtle.digest('SHA-256', new TextEncoder().encode('newpassword'))) const ADMIN_PWD_HASH = 'f7e29ca28134088a79623d800bf1204d14032dd0c445bf569e91fcdd23f1c4d4'; let _drawLayers = []; // saved drawing layers // ── Admin: Publish & Sync ───────────────────────────────── async function adminPublish(){ const btn = document.getElementById('admin-publish-btn'); const resetBtn = () => { if(btn){ btn.disabled=false; btn.innerHTML='↑ Publicar cambios'; } }; if(btn){ btn.disabled=true; btn.innerHTML='Publicando...'; } if(!ADMIN_SECRET){ document.getElementById('admin-jsonbin-setup').style.display = 'block'; _showSyncStatus('⚠ ADMIN_SECRET no configurado', 'var(--amb)'); resetBtn(); return; } try{ const data = _getConflicts(); const ok = await _saveConflicts(data); _showSyncStatus( ok ? '✓ Publicado — '+data.length+' conflictos en GitHub' : '✗ Error — revisa la consola (F12)', ok ? 'var(--grn)' : 'var(--red)' ); }catch(e){ console.error('adminPublish error:', e); _showSyncStatus('✗ Error: '+e.message, 'var(--red)'); } finally { resetBtn(); } } function _showSyncStatus(msg, col){ col = col || 'var(--grn)'; const el = document.getElementById('admin-sync-status'); if(!el) return; el.textContent = msg; el.style.color = col; setTimeout(() => { if(el.textContent===msg) el.textContent=''; }, 5000); } async function _pollRemoteConflicts(){ if(_adminAuthed) return; try{ const r = await fetch(WORKER_CONFLICTS_GET + '?t=' + Math.floor(Date.now()/60000), { signal: AbortSignal.timeout(6000), }); if(!r.ok) return; const data = await r.json(); if(!Array.isArray(data)) return; if(!Array.isArray(data) || !data.length) return; const current = JSON.stringify(_conflicts); if(JSON.stringify(data) !== current){ _conflicts = data; localStorage.setItem(SK_CONFLICTS, JSON.stringify(data)); _drawConflicts(); } }catch(e){} } // Storage keys const SK_CONFLICTS = 'ct_map_conflicts'; const SK_DRAWINGS = 'ct_map_drawings'; // ── Open admin panel ────────────────────────────────────── function openAdminPanel(){ openPanel('admin'); if(_adminAuthed){ document.getElementById('admin-login').style.display = 'none'; document.getElementById('admin-workspace').style.display = 'flex'; _renderAdminList(); _loadDrawingsFromStorage(); } } // ── Login ───────────────────────────────────────────────── async function adminLogin(){ const pwd = document.getElementById('admin-pwd').value; const err = document.getElementById('admin-err'); if(!pwd){ err.style.display='block'; return; } // Hash the input and compare const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(pwd)); const hash = Array.from(new Uint8Array(buf)).map(b=>b.toString(16).padStart(2,'0')).join(''); if(hash === ADMIN_PWD_HASH){ _adminAuthed = true; err.style.display = 'none'; document.getElementById('admin-login').style.display = 'none'; document.getElementById('admin-workspace').style.display = 'flex'; _renderAdminList(); _loadDrawingsFromStorage(); } else { err.style.display = 'block'; document.getElementById('admin-pwd').value = ''; } } // ── Admin tabs ──────────────────────────────────────────── function admTab(tab, btn){ document.querySelectorAll('.adm-tab').forEach(b => { b.style.borderBottomColor = 'transparent'; b.style.color = 'var(--t2)'; b.classList.remove('on'); }); btn.style.borderBottomColor = 'var(--t0)'; btn.style.color = 'var(--t1)'; btn.classList.add('on'); ['list','add','draw'].forEach(t => { const el = document.getElementById('adm-'+t); if(el) el.style.display = t===tab ? 'block' : 'none'; }); if(tab==='list') _renderAdminList(); if(tab==='draw') _loadDrawingsFromStorage(); } // ── Conflict list ───────────────────────────────────────── function _renderAdminList(){ const el = document.getElementById('admin-conflict-list'); if(!el) return; const data = _getConflicts(); el.innerHTML = data.map(d => { const col = ['#4d8cf5','#f0a832','#e8445a'][d.i] || '#4d8cf5'; return `
${d.name}
${d.lat.toFixed(1)}, ${d.lng.toFixed(1)} · ${d.since||''}
`; }).join(''); } // ── Edit conflict ───────────────────────────────────────── function adminEditConflict(id){ const data = _getConflicts(); const d = data.find(x => x.id === id); if(!d) return; _editingId = id; document.getElementById('ae-id').value = id; document.getElementById('ae-name').value = d.name; document.getElementById('ae-lat').value = d.lat; document.getElementById('ae-lng').value = d.lng; document.getElementById('ae-intensity').value = d.i; document.getElementById('ae-since').value = d.since || ''; document.getElementById('ae-parties').value = d.parties || ''; document.getElementById('ae-deaths').value = d.deaths || ''; document.getElementById('ae-summary').value = d.summary || ''; document.getElementById('ae-delete-btn').style.display = 'block'; // Fly to conflict on map // Switch to add/edit tab admTab('add', document.getElementById('atab-add')); } // ── New conflict ────────────────────────────────────────── function adminNewConflict(){ _editingId = null; ['ae-id','ae-name','ae-lat','ae-lng','ae-since','ae-parties','ae-deaths','ae-summary'] .forEach(id => { const el=document.getElementById(id); if(el) el.value=''; }); document.getElementById('ae-intensity').value = '1'; document.getElementById('ae-delete-btn').style.display = 'none'; admTab('add', document.getElementById('atab-add')); } // ── Save conflict ───────────────────────────────────────── function adminSaveConflict(){ const name = document.getElementById('ae-name').value.trim(); const lat = parseFloat(document.getElementById('ae-lat').value); const lng = parseFloat(document.getElementById('ae-lng').value); const intensity = parseInt(document.getElementById('ae-intensity').value); const since = document.getElementById('ae-since').value.trim(); const parties = document.getElementById('ae-parties').value.trim(); const deaths = document.getElementById('ae-deaths').value.trim(); const summary = document.getElementById('ae-summary').value.trim(); if(!name || isNaN(lat) || isNaN(lng)){ _showAEMsg('Nombre, latitud y longitud son obligatorios.', 'var(--red)'); return; } if(lat < -90||lat > 90||lng < -180||lng > 180){ _showAEMsg('Coordenadas fuera de rango.', 'var(--red)'); return; } const data = _getConflicts(); const id = _editingId || 'custom_' + Date.now(); const entry = {id, name, lat, lng, i:intensity, since, parties, deaths, summary, cat:'general', _custom:true}; if(_editingId){ const idx = data.findIndex(x => x.id === _editingId); if(idx >= 0) data[idx] = entry; } else { data.push(entry); } _saveConflicts(data).then(()=>{}); // async shared storage _conflicts = data; _drawConflicts(); _showAEMsg('✓ Guardado (compartido)', 'var(--grn)'); setTimeout(() => admTab('list', document.getElementById('atab-list')), 800); } // ── Delete conflict ─────────────────────────────────────── function adminDeleteConflict(){ if(!_editingId) return; if(!confirm('¿Eliminar este conflicto?')) return; const data = _getConflicts().filter(x => x.id !== _editingId); _saveConflicts(data); // async _conflicts = data; _drawConflicts(); _editingId = null; admTab('list', document.getElementById('atab-list')); } // ── Reset to defaults ───────────────────────────────────── function adminResetToDefault(){ if(!confirm('¿Restaurar el dataset original? Se perderán todos los cambios.')) return; localStorage.removeItem(SK_CONFLICTS); try{ window.storage.delete('map_conflicts', true); }catch(e){} _conflicts = _getConflicts(); _drawConflicts(); _renderAdminList(); } // ── Pick coordinates on map ─────────────────────────────── function adminPickOnMap(){ alert('El mapa ha sido desactivado.'); return; _pickingCoord = true; const btn = document.getElementById('pick-map-btn'); btn.textContent = '🎯 Clic en el mapa...'; btn.style.background = 'rgba(232,68,90,.1)'; btn.style.borderColor = 'var(--red)'; const handler = e => { document.getElementById('ae-lat').value = e.latlng.lat.toFixed(4); document.getElementById('ae-lng').value = e.latlng.lng.toFixed(4); btn.textContent = `📍 ${e.latlng.lat.toFixed(3)}, ${e.latlng.lng.toFixed(3)}`; btn.style.background = 'rgba(45,212,160,.1)'; btn.style.borderColor = 'var(--grn)'; btn.style.color = 'var(--grn)'; _pickingCoord = false; // Revert after 3s setTimeout(() => { btn.textContent = '📍 Clicar en el mapa para seleccionar coordenadas'; btn.style.background = ''; btn.style.borderColor = 'var(--b0)'; btn.style.color = 'var(--t0)'; }, 3000); }; } // ── DRAW MODE ───────────────────────────────────────────── function setDrawColor(el, col){ _drawColor = col; document.querySelectorAll('.draw-col-btn').forEach(b => b.style.border = '2px solid transparent'); el.style.border = '2px solid #fff'; } // ── GitHub — shared storage via repo file ───────────────── // Setup: github.com → Settings → Developer settings → Personal access tokens // → Fine-grained token → repo: lokiff/worldmonitor → permisos: Contents (read+write) // GitHub config moved to Cloudflare Worker environment variables async function _getConflictsAsync(){ // 1. Worker GET /conflicts — sin auth, datos en GitHub via Worker try{ const r = await fetch(WORKER_CONFLICTS_GET + '?t=' + Math.floor(Date.now()/60000), { signal: AbortSignal.timeout(5000) }); if(r.ok){ const data = await r.json(); if(Array.isArray(data) && data.length){ localStorage.setItem(SK_CONFLICTS, JSON.stringify(data)); return data; } } }catch(e){} // 2. localStorage const raw = localStorage.getItem(SK_CONFLICTS); if(raw){ try{ const d=JSON.parse(raw); if(d.length) return d; }catch(e){} } // 3. Hardcoded defaults return []; } function _getConflicts(){ const raw = localStorage.getItem(SK_CONFLICTS); if(raw) try{ return JSON.parse(raw); }catch(e){} return []; } async function _saveConflicts(data){ localStorage.setItem(SK_CONFLICTS, JSON.stringify(data)); if(!ADMIN_SECRET){ console.warn('ADMIN_SECRET not set — saved locally only'); return true; } try{ const res = await fetch(WORKER_CONFLICTS_PUT, { method: 'PUT', headers:{ 'Content-Type': 'application/json', 'X-Admin-Secret': ADMIN_SECRET, }, body: JSON.stringify(data), signal: AbortSignal.timeout(12000), }); if(res.ok){ console.log('✓ conflicts.json publicado via Worker'); return true; } const err = await res.json().catch(() => ({})); console.error('Worker PUT error:', res.status, err); _showSyncStatus('✗ Error '+res.status+': '+(err.error||''), 'var(--red)'); return false; }catch(e){ console.error('Worker exception:', e); _showSyncStatus('✗ Error de red: '+e.message, 'var(--red)'); return false; } } function _showAEMsg(msg, col){ const el = document.getElementById('ae-msg'); if(!el) return; el.textContent = msg; el.style.color = col; el.style.display = 'block'; setTimeout(() => { el.style.display='none'; }, 2500); } // ═══════════════════════════════════════════════════════════ // MOBILE TAB NAVIGATION // ═══════════════════════════════════════════════════════════ function mobTab(which, btn){ // Update tab buttons document.querySelectorAll('.mob-tab').forEach(b => b.classList.remove('on')); btn.classList.add('on'); // Show correct column document.querySelectorAll('.col').forEach(c => c.classList.remove('mob-active')); const col = document.getElementById('col-' + which); if(col) col.classList.add('mob-active'); } // Keep mobile badges in sync with col badges function syncMobBadges(){ const mn = document.getElementById('ft-media'); const mo = document.getElementById('ft-osint'); const bn = document.getElementById('mbadge-news'); const bo = document.getElementById('mbadge-osint'); if(mn && bn) bn.textContent = mn.textContent || '—'; if(mo && bo) bo.textContent = mo.textContent || '—'; } function init(){ loadPrefs(); startClock(); renderSaved(); renderCustomList(); // Initial load progSet(2); setTimeout(async ()=>{ await Promise.all([loadMedia(), loadOSINT()]); setLastUpdated(); setTimeout(loadBreakingBar, 300); setTimeout(loadGDELT, 800); setTimeout(loadOREF, 1200); }, 300); // Background refresh — tab-aware, no countdown startAutoRefresh(); renderConflicts(); setInterval(loadOREF, 15000); setInterval(loadGDELT, 15*60*1000); setInterval(_pollRemoteConflicts, 5*60*1000); } document.addEventListener('DOMContentLoaded', init);