MediaWiki:Common.js: Difference between revisions

No edit summary
Add geo dashboard script: Users by Country with day/week/month periods, activity chart, world map
 
(11 intermediate revisions by the same user not shown)
Line 1: Line 1:
// Hero logo image styling - white + orange glow
(function() {
var s = document.createElement('style');
s.textContent = '#ax-hero-logo img { filter: hue-rotate(200deg) saturate(8) brightness(1.6) drop-shadow(0 0 20px rgba(255,120,0,0.9)) drop-shadow(0 0 50px rgba(255,80,0,0.5)); max-width:520px; width:80%; height:auto; display:inline-block; }';
document.head.appendChild(s);
})();
// Knowledge Areas grid layout fixer
// Knowledge Areas grid layout fixer
(function() {
(function() {
Line 641: Line 648:
   } else {
   } else {
     applyCatImages();
     applyCatImages();
  }
})();
// Add Impressum link to footer places row
(function() {
  function addImpressumToFooter() {
    // Find the footer-places list
    var footerPlaces = document.getElementById('footer-places');
    if (!footerPlaces) return;
    // Check if Impressum link already exists
    if (document.getElementById('footer-places-impressum')) return;
    // Create the new list item
    var li = document.createElement('li');
    li.id = 'footer-places-impressum';
    var a = document.createElement('a');
    a.href = '/Impressum';
    a.textContent = 'Impressum';
    li.appendChild(a);
    footerPlaces.appendChild(li);
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', addImpressumToFooter);
  } else {
    addImpressumToFooter();
  }
})();
// ===== Permanent Favicon =====
(function() {
  document.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"]').forEach(function(el) {
    el.parentNode.removeChild(el);
  });
  var link = document.createElement('link');
  link.rel = 'icon';
  link.type = 'image/png';
  link.href = 'https://alphax.wiki/images/2/26/Favicon.png';
  document.head.appendChild(link);
})();
// =====================================================
// Users by Country Counter - Admin Page Widget
// =====================================================
(function() {
  'use strict';
  if (mw.config.get('wgPageName') !== 'User:Admin') return;
  var langToCountry = {
    'en': '🇺🇸 English-speaking', 'de': '🇩🇪 Germany / German-speaking',
    'fr': '🇫🇷 France / French-speaking', 'es': '🇪🇸 Spain / Spanish-speaking',
    'pt': '🇵🇹 Portugal / Portuguese-speaking', 'it': '🇮🇹 Italy / Italian-speaking',
    'nl': '🇳🇱 Netherlands / Dutch-speaking', 'pl': '🇵🇱 Poland / Polish-speaking',
    'ru': '🇷🇺 Russia / Russian-speaking', 'ja': '🇯🇵 Japan',
    'zh': '🇨🇳 China / Chinese-speaking', 'ar': '🇸🇦 Arabic-speaking',
    'tr': '🇹🇷 Turkey / Turkish-speaking', 'sv': '🇸🇪 Sweden / Swedish-speaking',
    'da': '🇩🇰 Denmark / Danish-speaking', 'fi': '🇫🇮 Finland / Finnish-speaking',
    'nb': '🇳🇴 Norway / Norwegian-speaking', 'cs': '🇨🇿 Czech Republic',
    'hu': '🇭🇺 Hungary', 'ro': '🇷🇴 Romania', 'el': '🇬🇷 Greece / Greek-speaking',
    'ko': '🇰🇷 Korea / Korean-speaking', 'uk': '🇺🇦 Ukraine / Ukrainian-speaking',
    'he': '🇮🇱 Israel / Hebrew-speaking', 'id': '🇮🇩 Indonesia',
    'vi': '🇻🇳 Vietnam', 'th': '🇹🇭 Thailand', 'hi': '🇮🇳 India / Hindi-speaking',
    'bn': '🇧🇩 Bangladesh / Bengali-speaking', 'fa': '🇮🇷 Iran / Persian-speaking'
  };
  function renderResults(langCounts, totalUsers) {
    var container = document.getElementById('country-user-counter');
    if (!container) return;
    var sorted = Object.keys(langCounts)
      .filter(function(l) { return langCounts[l] > 0; })
      .sort(function(a, b) { return langCounts[b] - langCounts[a]; });
    var html = '<style>';
    html += '#cuc-wrap{font-family:sans-serif}';
    html += '#cuc-stats{display:flex;gap:20px;margin-bottom:14px;flex-wrap:wrap}';
    html += '.cuc-stat-card{background:#222;border:1px solid #444;border-radius:6px;padding:10px 18px;min-width:120px;text-align:center}';
    html += '.cuc-stat-num{font-size:1.8em;font-weight:bold;color:#e07000}';
    html += '.cuc-stat-lbl{font-size:0.8em;color:#aaa;margin-top:2px}';
    html += '#cuc-table{width:100%;border-collapse:collapse;margin-top:4px}';
    html += '#cuc-table th{background:#2a2a2a;color:#e07000;padding:8px 12px;text-align:left;border:1px solid #444;font-size:0.9em}';
    html += '#cuc-table td{padding:7px 12px;border:1px solid #2e2e2e;color:#ddd;font-size:0.9em}';
    html += '#cuc-table tr:nth-child(even) td{background:#1c1c1c}';
    html += '#cuc-table tr:hover td{background:#252525}';
    html += '.cuc-bar-wrap{background:#111;border-radius:3px;height:14px;overflow:hidden}';
    html += '.cuc-bar{height:14px;background:linear-gradient(90deg,#e07000,#ff9a00);border-radius:3px;transition:width 0.4s ease}';
    html += '.cuc-no-data{background:#1a1a1a;border:1px dashed #444;border-radius:6px;padding:16px;color:#aaa;line-height:1.6}';
    html += '.cuc-no-data code{background:#222;padding:2px 6px;border-radius:3px;color:#e07000;font-size:0.9em}';
    html += '#cuc-timestamp{color:#555;font-size:0.8em;margin-top:10px}';
    html += '</style>';
    html += '<div id="cuc-wrap">';
    // Stats cards
    html += '<div id="cuc-stats">';
    html += '<div class="cuc-stat-card"><div class="cuc-stat-num">' + totalUsers + '</div><div class="cuc-stat-lbl">Total Users</div></div>';
    html += '<div class="cuc-stat-card"><div class="cuc-stat-num">' + sorted.length + '</div><div class="cuc-stat-lbl">Countries / Regions</div></div>';
    var locatedUsers = sorted.reduce(function(sum, l) { return sum + langCounts[l]; }, 0);
    html += '<div class="cuc-stat-card"><div class="cuc-stat-num">' + locatedUsers + '</div><div class="cuc-stat-lbl">Users with Location</div></div>';
    html += '</div>';
    if (sorted.length === 0) {
      html += '<div class="cuc-no-data">';
      html += '<strong>📍 No country data available yet.</strong><br>';
      html += 'This counter tracks users by country using the <strong>Babel extension</strong> language categories.<br>';
      html += 'Users can declare their language/country by adding babel tags to their user page:<br><br>';
      html += '<code>{{#babel:en|de|fr}}</code><br><br>';
      html += 'Once users add babel tags, this counter will automatically display their country distribution.<br>';
      html += 'Currently tracking <strong>' + totalUsers + ' registered users</strong> on this wiki.';
      html += '</div>';
    } else {
      var maxCount = langCounts[sorted[0]] || 1;
      html += '<table id="cuc-table"><thead><tr>';
      html += '<th style="width:40px">#</th>';
      html += '<th>Country / Region</th>';
      html += '<th style="width:70px;text-align:center">Users</th>';
      html += '<th style="width:200px">Distribution</th>';
      html += '</tr></thead><tbody>';
      sorted.forEach(function(lang, idx) {
        var count = langCounts[lang];
        var label = langToCountry[lang] || ('🌐 ' + lang.toUpperCase());
        var pct = Math.round((count / locatedUsers) * 100);
        var barWidth = Math.round((count / maxCount) * 180);
        html += '<tr>';
        html += '<td style="color:#666;text-align:center">' + (idx + 1) + '</td>';
        html += '<td>' + label + '</td>';
        html += '<td style="text-align:center;font-weight:bold;color:#e07000">' + count + '</td>';
        html += '<td><div class="cuc-bar-wrap"><div class="cuc-bar" style="width:' + barWidth + 'px"></div></div></td>';
        html += '</tr>';
      });
      html += '</tbody></table>';
    }
    html += '<div id="cuc-timestamp">Last updated: ' + new Date().toLocaleString() + '</div>';
    html += '</div>';
    container.innerHTML = html;
  }
  function buildCounter() {
    var container = document.getElementById('country-user-counter');
    if (!container) return;
    container.innerHTML = '<p style="color:#aaa;font-style:italic">⏳ Loading user statistics by country...</p>';
    var allUsers = [];
    var langCounts = {};
    var langs = Object.keys(langToCountry);
    function fetchUsers(aufrom) {
      var params = { action: 'query', list: 'allusers', aulimit: 500, format: 'json' };
      if (aufrom) params.aufrom = aufrom;
      return $.getJSON(mw.util.wikiScript('api'), params).then(function(data) {
        var users = (data.query && data.query.allusers) || [];
        allUsers = allUsers.concat(users);
        if (data['continue'] && data['continue'].aufrom) return fetchUsers(data['continue'].aufrom);
      });
    }
    function fetchLangCat(lang) {
      return $.getJSON(mw.util.wikiScript('api'), {
        action: 'query', list: 'categorymembers',
        cmtitle: 'Category:User_' + lang,
        cmtype: 'page', cmnamespace: 2, cmlimit: 500, format: 'json'
      }).then(function(data) {
        var m = (data.query && data.query.categorymembers) || [];
        if (m.length > 0) langCounts[lang] = (langCounts[lang] || 0) + m.length;
      }).catch(function() {});
    }
    fetchUsers().then(function() {
      var d = $.Deferred().resolve().promise();
      langs.forEach(function(lang) { d = d.then(function() { return fetchLangCat(lang); }); });
      return d;
    }).then(function() {
      renderResults(langCounts, allUsers.length);
    }).catch(function(e) {
      container.innerHTML = '<p style="color:#f66">⚠️ Error loading data: ' + String(e) + '</p>';
    });
  }
  $(document).ready(function() { buildCounter(); });
})();
// =====================================================
// End: Users by Country Counter
// =====================================================
// ===== Article Overview Page - Category/Subcategory/Title/Words/Focus Table =====
(function() {
  if (mw.config.get('wgPageName') !== 'Article_Overview') return;
  var apiBase = mw.util.wikiScript('api');
  var allRows = [];
  var tableBody = null;
  var statusSpan = null;
  $(document).ready(function() {
    var wrapper = document.getElementById('article-overview-wrapper');
    if (!wrapper) return;
    // Build controls row with button + status
    var controls = document.createElement('div');
    controls.style.marginBottom = '14px';
    var btn = document.createElement('button');
    btn.textContent = 'Download CSV';
    btn.style.cssText = 'background-color:#e07b00;color:#fff;padding:9px 20px;border:none;border-radius:5px;font-size:14px;font-weight:bold;cursor:pointer;letter-spacing:0.3px;';
    btn.addEventListener('click', downloadCSV);
    controls.appendChild(btn);
    statusSpan = document.createElement('span');
    statusSpan.style.cssText = 'margin-left:14px;font-size:13px;color:#aaa;vertical-align:middle;';
    statusSpan.textContent = 'Loading…';
    controls.appendChild(statusSpan);
    wrapper.appendChild(controls);
    // Build table
    var table = document.createElement('table');
    table.style.cssText = 'width:100%;border-collapse:collapse;font-size:13px;';
    var thead = document.createElement('thead');
    var headerRow = document.createElement('tr');
    headerRow.style.cssText = 'background-color:#2a2a2a;color:#f0a500;text-align:left;';
    var cols = ['Category', 'Subcategory', 'Title', 'Words', 'Focus'];
    cols.forEach(function(colName, i) {
      var th = document.createElement('th');
      th.textContent = colName;
      th.style.cssText = 'padding:9px 12px;border:1px solid #444;' + (colName === 'Words' ? 'text-align:right;' : '');
      headerRow.appendChild(th);
    });
    thead.appendChild(headerRow);
    table.appendChild(thead);
    tableBody = document.createElement('tbody');
    tableBody.innerHTML = '<tr><td colspan="5" style="padding:12px;text-align:center;color:#aaa;font-style:italic;">Loading articles… please wait.</td></tr>';
    table.appendChild(tableBody);
    wrapper.appendChild(table);
    buildTable();
  });
  function getCategoryMembers(catName, cmtype, continueParam, accumulated, callback) {
    var params = {
      action: 'query', list: 'categorymembers',
      cmtitle: 'Category:' + catName,
      cmlimit: 500, cmtype: cmtype, format: 'json'
    };
    if (continueParam) params.cmcontinue = continueParam;
    $.getJSON(apiBase, params).done(function(data) {
      var members = (data.query && data.query.categorymembers) ? data.query.categorymembers : [];
      members.forEach(function(m){ accumulated.push(m); });
      if (data.continue && data.continue.cmcontinue) {
        getCategoryMembers(catName, cmtype, data.continue.cmcontinue, accumulated, callback);
      } else {
        callback(accumulated);
      }
    }).fail(function(){ callback(accumulated); });
  }
  function escHtml(str) {
    return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
  }
  function renderTable(rows) {
    if (!tableBody) return;
    if (!rows || rows.length === 0) {
      tableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;text-align:center;color:#aaa;">No articles found.</td></tr>';
      return;
    }
    var html = '';
    rows.forEach(function(row, i) {
      var bg = i % 2 === 0 ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.07)';
      html += '<tr style="background:' + bg + ';">';
      html += '<td style="padding:6px 10px;border:1px solid #444;">' + escHtml(row.category) + '</td>';
      html += '<td style="padding:6px 10px;border:1px solid #444;">' + escHtml(row.sub) + '</td>';
      html += '<td style="padding:6px 10px;border:1px solid #444;"><a href="' + mw.util.getUrl(row.title) + '">' + escHtml(row.title) + '</a></td>';
      html += '<td style="padding:6px 10px;border:1px solid #444;text-align:right;">' + escHtml(String(row.words)) + '</td>';
      html += '<td style="padding:6px 10px;border:1px solid #444;">' + escHtml(row.focus) + '</td>';
      html += '</tr>';
    });
    tableBody.innerHTML = html;
  }
  function buildTable() {
    if (statusSpan) statusSpan.textContent = 'Fetching categories…';
    $.getJSON(apiBase, { action:'query', list:'allcategories', aclimit:500, format:'json' })
      .done(function(data) {
        var topCats = (data.query && data.query.allcategories) ? data.query.allcategories.map(function(c){ return c['*']; }) : [];
        var rows = [];
        var totalCats = topCats.length;
        var processed = 0;
        if (totalCats === 0) {
          if (tableBody) tableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;text-align:center;color:#aaa;">No categories found.</td></tr>';
          if (statusSpan) statusSpan.textContent = '';
          return;
        }
        function checkDone() {
          processed++;
          if (statusSpan) statusSpan.textContent = 'Loading… (' + processed + '/' + totalCats + ' categories)';
          if (processed === totalCats) {
            rows.sort(function(a,b){
              var ca = a.category.toLowerCase(), cb = b.category.toLowerCase();
              if (ca < cb) return -1; if (ca > cb) return 1;
              var sa = a.sub.toLowerCase(), sb = b.sub.toLowerCase();
              if (sa < sb) return -1; if (sa > sb) return 1;
              return a.title.localeCompare(b.title);
            });
            allRows = rows;
            renderTable(rows);
            if (statusSpan) statusSpan.textContent = rows.length + ' article entries loaded.';
          }
        }
        topCats.forEach(function(catName) {
          getCategoryMembers(catName, 'page|subcat', null, [], function(members) {
            var pages = members.filter(function(m){ return m.ns === 0; });
            var subcats = members.filter(function(m){ return m.ns === 14; });
            if (pages.length === 0 && subcats.length === 0) {
              checkDone(); return;
            }
            var subTotal = pages.length + subcats.length;
            var subProcessed = 0;
            function subDone() {
              subProcessed++;
              if (subProcessed >= subTotal) checkDone();
            }
            pages.forEach(function(page) {
              rows.push({ category: catName, sub: '—', title: page.title, words: 'n/a', focus: catName });
              subDone();
            });
            subcats.forEach(function(subcat) {
              var subName = subcat.title.replace('Category:','');
              getCategoryMembers(subName, 'page', null, [], function(subPages) {
                subPages.filter(function(m){ return m.ns === 0; }).forEach(function(page) {
                  rows.push({ category: catName, sub: subName, title: page.title, words: 'n/a', focus: catName });
                });
                subDone();
              });
            });
          });
        });
      })
      .fail(function() {
        if (tableBody) tableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;text-align:center;color:#f55;">Error loading data. Check API access.</td></tr>';
      });
  }
  function downloadCSV() {
    if (!allRows || allRows.length === 0) {
      alert('No data to download yet. Please wait for the table to finish loading.');
      return;
    }
    var csv = 'Category,Subcategory,Title,Words,Focus\n';
    allRows.forEach(function(row) {
      csv += ['category','sub','title','words','focus'].map(function(k){
        return '"' + String(row[k]).replace(/"/g,'""') + '"';
      }).join(',') + '\n';
    });
    var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
    var url = URL.createObjectURL(blob);
    var a = document.createElement('a');
    a.href = url; a.download = 'article-overview.csv';
    document.body.appendChild(a); a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }
})();
// ═══════════════════════════════════════════════════════════
// GEO DASHBOARD – Users by Country (Day / Week / Month)
// ═══════════════════════════════════════════════════════════
(function () {
  if (!document.getElementById('geo-dashboard')) return;
  var API = '/api.php';
  function isoDate(d) { return d.toISOString().slice(0, 10); }
  function startOf(period) {
    var now = new Date();
    if (period === 'day') return new Date(now.getFullYear(), now.getMonth(), now.getDate());
    if (period === 'week') { var d = new Date(now); d.setDate(d.getDate() - d.getDay()); d.setHours(0,0,0,0); return d; }
    if (period === 'month') return new Date(now.getFullYear(), now.getMonth(), 1);
    return new Date(0);
  }
  var COUNTRY_MAP = {
    'en':'🇺🇸 English / US','de':'🇩🇪 German','fr':'🇫🇷 French','es':'🇪🇸 Spanish',
    'it':'🇮🇹 Italian','pt':'🇧🇷 Portuguese','ru':'🇷🇺 Russian','ja':'🇯🇵 Japanese',
    'zh':'🇨🇳 Chinese','ko':'🇰🇷 Korean','ar':'🇸🇦 Arabic','nl':'🇳🇱 Dutch',
    'pl':'🇵🇱 Polish','sv':'🇸🇪 Swedish','no':'🇳🇴 Norwegian','da':'🇩🇰 Danish',
    'fi':'🇫🇮 Finnish','tr':'🇹🇷 Turkish','cs':'🇨🇿 Czech','hu':'🇭🇺 Hungarian',
    'ro':'🇷🇴 Romanian','uk':'🇺🇦 Ukrainian','vi':'🇻🇳 Vietnamese','th':'🇹🇭 Thai',
    'id':'🇮🇩 Indonesian','ms':'🇲🇾 Malay','he':'🇮🇱 Hebrew','fa':'🇮🇷 Persian',
    'hi':'🇮🇳 Hindi','bn':'🇧🇩 Bengali'
  };
  function fetchUsers() {
    return fetch(API + '?action=query&list=allusers&aulimit=500&format=json')
      .then(function(r) { return r.json(); })
      .then(function(d) { return d.query.allusers || []; });
  }
  function fetchChanges() {
    return fetch(API + '?action=query&list=recentchanges&rclimit=500&rcprop=user%7Ctimestamp&format=json')
      .then(function(r) { return r.json(); })
      .then(function(d) { return d.query.recentchanges || []; });
  }
  function fetchBabel(username) {
    return fetch(API + '?action=query&titles=User:' + encodeURIComponent(username) + '&prop=categories&format=json')
      .then(function(r) { return r.json(); })
      .then(function(d) {
        var pages = (d.query && d.query.pages) ? d.query.pages : {};
        var langs = [];
        Object.values(pages).forEach(function(p) {
          (p.categories || []).forEach(function(c) {
            var m = c.title.match(/Category:User ([a-z]{2,3})/i);
            if (m) langs.push(m[1].toLowerCase());
          });
        });
        return langs;
      });
  }
  function drawMap(activeLangs) {
    var svg = document.getElementById('geo-world-map');
    if (!svg) return;
    svg.innerHTML = '';
    var bg = document.createElementNS('http://www.w3.org/2000/svg','rect');
    bg.setAttribute('width','900'); bg.setAttribute('height','440'); bg.setAttribute('fill','#0d1f33');
    svg.appendChild(bg);
    var continents = [
      'M80,100 L240,95 L255,200 L220,230 L170,235 L110,220 L75,170 Z',
      'M155,240 L220,235 L235,360 L195,385 L155,370 L138,320 Z',
      'M385,90 L480,85 L490,160 L450,175 L400,170 L378,140 Z',
      'M390,175 L460,170 L470,320 L430,345 L388,335 L375,280 Z',
      'M490,80 L750,75 L760,230 L700,250 L520,240 L485,180 Z',
      'M640,280 L740,275 L748,355 L700,368 L638,355 Z',
      'M230,50 L290,45 L295,90 L255,95 L228,82 Z'
    ];
    continents.forEach(function(d) {
      var path = document.createElementNS('http://www.w3.org/2000/svg','path');
      path.setAttribute('d', d); path.setAttribute('fill','#2a3a2a');
      path.setAttribute('stroke','#1a2a1a'); path.setAttribute('stroke-width','1');
      svg.appendChild(path);
    });
    var shapes = {
      'de':'M440,145 L455,140 L465,148 L462,165 L448,170 L438,162 Z',
      'fr':'M415,152 L430,148 L438,162 L430,175 L415,172 L408,162 Z',
      'es':'M390,170 L415,168 L415,185 L395,190 L385,182 Z',
      'it':'M450,168 L462,165 L468,180 L460,195 L450,188 L445,178 Z',
      'gb':'M418,135 L428,132 L430,145 L420,148 L414,142 Z',
      'ru':'M460,120 L560,110 L580,135 L540,145 L465,148 Z',
      'us':'M120,150 L220,148 L225,190 L200,200 L115,195 Z',
      'cn':'M620,150 L680,145 L685,175 L650,185 L615,178 Z',
      'jp':'M695,155 L708,152 L710,165 L700,168 L693,163 Z',
      'br':'M195,230 L240,225 L245,275 L215,282 L188,270 Z',
      'in':'M580,175 L615,170 L618,210 L595,218 L575,205 Z',
      'au':'M660,280 L720,275 L725,320 L690,328 L655,315 Z'
    };
    Object.keys(shapes).forEach(function(cc) {
      var isActive = activeLangs.some(function(l) { return l.startsWith(cc); });
      var path = document.createElementNS('http://www.w3.org/2000/svg','path');
      path.setAttribute('d', shapes[cc]);
      path.setAttribute('fill', isActive ? '#ff6600' : '#3a4a3a');
      path.setAttribute('stroke','#555'); path.setAttribute('stroke-width','1');
      path.setAttribute('opacity', isActive ? '0.9' : '0.5');
      svg.appendChild(path);
    });
    var labels = [{x:160,y:175,t:'North America'},{x:450,y:130,t:'Europe'},{x:620,y:165,t:'Asia'},
                  {x:430,y:265,t:'Africa'},{x:192,y:305,t:'South America'},{x:693,y:325,t:'Oceania'}];
    labels.forEach(function(loc) {
      var text = document.createElementNS('http://www.w3.org/2000/svg','text');
      text.setAttribute('x',loc.x); text.setAttribute('y',loc.y);
      text.setAttribute('fill','#444'); text.setAttribute('font-size','10');
      text.setAttribute('font-family','sans-serif'); text.setAttribute('text-anchor','middle');
      text.textContent = loc.t; svg.appendChild(text);
    });
  }
  function renderActivityBars(daily) {
    var wrap = document.getElementById('geo-activity-bars');
    var labelWrap = document.getElementById('geo-activity-labels');
    if (!wrap) return;
    var days = [];
    for (var i = 29; i >= 0; i--) {
      var d = new Date(); d.setDate(d.getDate() - i); days.push(isoDate(d));
    }
    var max = Math.max.apply(null, days.map(function(d){ return daily[d]||0; }).concat([1]));
    var bHtml = '', lHtml = '';
    days.forEach(function(day, idx) {
      var val = daily[day] || 0;
      var h = Math.max(3, Math.round((val/max)*70));
      var isToday = idx === 29;
      var col = isToday ? '#ff6600' : (val > 0 ? '#cc5500' : '#222');
      bHtml += '<div title="' + day + ': ' + val + ' edits" style="flex:1;background:' + col + ';height:' + h + 'px;border-radius:2px 2px 0 0;min-width:4px;"></div>';
      lHtml += '<div style="flex:1;text-align:center;white-space:nowrap;">' + (idx%5===0||isToday ? day.slice(5) : '') + '</div>';
    });
    wrap.innerHTML = bHtml; labelWrap.innerHTML = lHtml;
  }
  function renderTable(rows, total, period) {
    var tbody = document.getElementById('geo-table-body');
    var titleEl = document.getElementById('geo-table-title');
    if (!tbody) return;
    var labels = {day:'Today', week:'This Week', month:'This Month', all:'All Time'};
    if (titleEl) titleEl.textContent = 'Users by Country – ' + (labels[period]||'All Time');
    if (!rows.length) {
      tbody.innerHTML = '<tr><td colspan="5" style="padding:20px;text-align:center;color:#555;">No activity for this period.</td></tr>';
      return;
    }
    var html = '';
    rows.forEach(function(row, i) {
      var bar = '<div style="display:inline-block;background:#ff6600;height:8px;border-radius:2px;width:' + Math.round(row.pct) + '%;min-width:2px;vertical-align:middle;"></div><span style="color:#666;font-size:0.8em;margin-left:6px;">' + row.pct.toFixed(1) + '%</span>';
      html += '<tr style="background:' + (i%2===0?'#1a1a1a':'#1e1e1e') + ';border-top:1px solid #222;">' +
        '<td style="padding:10px 16px;color:#666;">' + (i+1) + '</td>' +
        '<td style="padding:10px 16px;">' + (COUNTRY_MAP[row.lang]||('🌐 '+row.lang)) + '</td>' +
        '<td style="padding:10px 16px;text-align:right;font-weight:bold;color:#ff6600;">' + row.users + '</td>' +
        '<td style="padding:10px 16px;">' + bar + '</td>' +
        '<td style="padding:10px 16px;text-align:right;color:#aaa;">' + row.edits + '</td></tr>';
    });
    tbody.innerHTML = html;
  }
  var cachedUsers = null, cachedChanges = null, cachedBabel = {}, currentPeriod = 'day';
  function showPeriod(period) {
    currentPeriod = period;
    ['day','week','month','all'].forEach(function(p) {
      var btn = document.getElementById('geo-btn-'+p);
      if (!btn) return;
      if (p === period) {
        btn.style.background='#ff6600'; btn.style.color='#fff';
        btn.style.borderColor='#ff6600'; btn.style.fontWeight='bold';
      } else {
        btn.style.background='#333'; btn.style.color='#eee';
        btn.style.borderColor='#555'; btn.style.fontWeight='normal';
      }
    });
    if (!cachedChanges || !cachedUsers) return;
    var cutoff = period === 'all' ? '1970-01-01' : startOf(period).toISOString();
    var userEdits = {};
    cachedChanges.forEach(function(c) {
      if (c.timestamp >= cutoff) userEdits[c.user] = (userEdits[c.user]||0)+1;
    });
    var langUsers = {}, langEdits = {};
    cachedUsers.forEach(function(u) {
      var langs = cachedBabel[u.name] || [];
      if (!langs.length) langs = ['unknown'];
      var edits = userEdits[u.name] || 0;
      var lang = langs[0];
      langUsers[lang] = (langUsers[lang]||0)+1;
      langEdits[lang] = (langEdits[lang]||0)+edits;
    });
    var total = cachedUsers.length;
    var cEl = document.getElementById('geo-countries');
    if (cEl) cEl.textContent = Object.keys(langUsers).filter(function(l){return l!=='unknown';}).length;
    var rows = Object.keys(langUsers).map(function(lang) {
      return {lang:lang, users:langUsers[lang], edits:langEdits[lang]||0, pct:total>0?(langUsers[lang]/total*100):0};
    }).sort(function(a,b) {
      if (a.lang==='unknown') return 1; if (b.lang==='unknown') return -1; return b.users-a.users;
    });
    renderTable(rows, total, period);
    drawMap(rows.filter(function(r){return r.lang!=='unknown';}).map(function(r){return r.lang;}));
  }
  window.geoShowPeriod = showPeriod;
  function boot() {
    var el = document.getElementById('geo-dashboard');
    if (!el) return;
    Promise.all([fetchUsers(), fetchChanges()]).then(function(results) {
      cachedUsers = results[0]; cachedChanges = results[1];
      var now = new Date();
      var tEl = document.getElementById('geo-total');
      if (tEl) tEl.textContent = cachedUsers.length;
      var updEl = document.getElementById('geo-updated');
      if (updEl) updEl.textContent = 'Updated: ' + now.toLocaleTimeString();
      var dStart = startOf('day').toISOString();
      var wStart = startOf('week').toISOString();
      var mStart = startOf('month').toISOString();
      var aDay=new Set(), aWeek=new Set(), aMonth=new Set();
      var daily = {};
      cachedChanges.forEach(function(c) {
        if (c.timestamp >= dStart) aDay.add(c.user);
        if (c.timestamp >= wStart) aWeek.add(c.user);
        if (c.timestamp >= mStart) aMonth.add(c.user);
        var day = c.timestamp.slice(0,10);
        daily[day] = (daily[day]||0)+1;
      });
      var adEl = document.getElementById('geo-active-day');
      var awEl = document.getElementById('geo-active-week');
      var amEl = document.getElementById('geo-active-month');
      if (adEl) adEl.textContent = aDay.size;
      if (awEl) awEl.textContent = aWeek.size;
      if (amEl) amEl.textContent = aMonth.size;
      renderActivityBars(daily);
      var babelPromises = cachedUsers.map(function(u) {
        return fetchBabel(u.name).then(function(langs) { cachedBabel[u.name] = langs; });
      });
      Promise.all(babelPromises).then(function() { showPeriod('day'); });
      // wire up buttons
      ['day','week','month','all'].forEach(function(p) {
        var btn = document.getElementById('geo-btn-'+p);
        if (btn) btn.addEventListener('click', function() { showPeriod(p); });
      });
    }).catch(function(e) { console.error('GEO dashboard error', e); });
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', boot);
  } else {
    setTimeout(boot, 100);
   }
   }
})();
})();