MediaWiki:Common.js: Difference between revisions

Add Admin Control Panel JS module — live article table with sort, filter, word count, categories, watchers, revisions
Add geo dashboard script: Users by Country with day/week/month periods, activity chart, world map
 
(31 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
(function() {
  function fixKnowledgeGrid() {
    var heroDiv = document.getElementById('ax-sexual-health-hero');
    if (!heroDiv) return;
    var firstCard = heroDiv.closest('.ax-card');
    if (!firstCard) return;
    var gridContainer = firstCard.parentElement;
    if (!gridContainer) return;
    gridContainer.style.display = 'grid';
    gridContainer.style.gridTemplateColumns = 'repeat(2, 1fr)';
    gridContainer.style.gap = '14px';
    // Collect cards by hero IDs
    var heroIds = ['ax-sexual-health-hero','ax-dating-hero','ax-kink-hero','ax-culture-hero','ax-fashion-hero','ax-community-hero','ax-drugs-hero','ax-life-hero'];
    var allKnowledgeCards = [];
    heroIds.forEach(function(id) {
      var h = document.getElementById(id);
      if (h) { var c = h.closest('.ax-card'); if (c) allKnowledgeCards.push(c); }
    });
    // Find remaining 3 knowledge cards by keyword match (Community, Drugs, Life Planning)
    // These have no hero images. Identify by checking they are NOT Start Learning or Featured
    var keywords = [];
    Array.from(document.querySelectorAll('.ax-card')).forEach(function(c) {
      if (allKnowledgeCards.indexOf(c) >= 0) return;
      var txt = c.innerText || '';
      keywords.forEach(function(kw) {
        if (txt.indexOf(kw) >= 0 && allKnowledgeCards.indexOf(c) < 0) {
          allKnowledgeCards.push(c);
        }
      });
    });
    // Move all knowledge cards into grid
    allKnowledgeCards.forEach(function(c) {
      if (c.parentElement !== gridContainer) gridContainer.appendChild(c);
    });
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', fixKnowledgeGrid);
  } else {
    fixKnowledgeGrid();
  }
})();
// Sexual Health hero image
(function() {
  var style = document.createElement('style');
  style.textContent = '#ax-sexual-health-hero { background-image: url("https://alphax.wiki/images/c/c2/Sexual_health.png"); background-size: cover; background-position: center center; } #ax-dating-hero { background-image: url("https://alphax.wiki/images/6/6c/Dating_and_relationships.png"); background-size: cover; background-position: center center; } #ax-kink-hero { background-image: url("https://alphax.wiki/images/e/e5/Kink_BDSM_Hero.png"); background-size: cover; background-position: center center; } #ax-culture-hero { background-image: url("https://alphax.wiki/images/2/25/Culture_History_Politics_Hero.png"); background-size: cover; background-position: center center; } #ax-fashion-hero { background-image: url("https://alphax.wiki/images/4/4b/Fashion_Visual_Signaling_Hero.png"); background-size: cover; background-position: center top; } #ax-community-hero { background-image: url("https://alphax.wiki/images/e/ed/Community_Identity_Hero.png"); background-size: cover; background-position: center center; } #ax-drugs-hero { background-image: url("https://alphax.wiki/images/c/c7/Drugs_Party_Culture_Hero.jpg"); background-size: cover; background-position: center center; } #ax-life-hero { background-image: url("https://alphax.wiki/images/7/74/Life_Planning_Hero.jpg"); background-size: cover; background-position: center top; }';
  document.head.appendChild(style);
})();
(function () {
(function () {


Line 258: Line 315:




/* ============================================ */
/* ================================================ */
/* ADMIN CONTROL PANEL AlphaX Wiki           */
/* ADMIN CONTROL PANEL - AlphaX Wiki               */
/* Runs only on AlphaX:Admin_Control_Panel page */
/* ================================================ */
/* ============================================ */
(function() {
(function() {
   if (mw.config.get('wgPageName') !== 'AlphaX:Admin_Control_Panel') return;
   if (mw.config.get('wgPageName') !== 'AlphaX:Admin_Control_Panel') return;
Line 267: Line 323:
   mw.loader.using(['mediawiki.api'], function() {
   mw.loader.using(['mediawiki.api'], function() {


    // Inject styles
     var css = [
     var css = `
       '#axcp-root *{box-sizing:border-box}',
       #axcp-root * { box-sizing: border-box; }
       '#axcp-root{font-family:system-ui,-apple-system,sans-serif;background:#0D0D0D;color:#fff;margin:-8px -20px;padding:0;border-radius:12px;overflow:hidden}',
       #axcp-root { font-family: system-ui,-apple-system,sans-serif; background: #0D0D0D; color: #fff; margin: -8px -20px; padding: 0; border-radius: 12px; overflow: hidden; }
       '.axcp-hdr{background:#1A1A1A;border-bottom:1px solid #2E2E2E;padding:24px 28px 20px}',
       .axcp-header { background: #1A1A1A; border-bottom: 1px solid #2E2E2E; padding: 24px 28px 20px; }
       '.axcp-ttl{font-size:22px;font-weight:700;color:#fff;margin-bottom:3px}',
       .axcp-title { font-size: 22px; font-weight: 700; color: #fff; margin-bottom: 3px; }
       '.axcp-sub{font-size:11px;color:#555;text-transform:uppercase;letter-spacing:.1em}',
       .axcp-subtitle { font-size: 11px; color: #555; text-transform: uppercase; letter-spacing: .1em; }
       '.axcp-sr{display:flex;gap:12px;margin:16px 0 0;flex-wrap:wrap}',
       .axcp-stats-row { display: flex; gap: 12px; margin: 18px 0 0; flex-wrap: wrap; }
       '.axcp-sc{background:#0D0D0D;border:1px solid #2E2E2E;border-radius:12px;padding:14px 18px;flex:1;min-width:90px}',
       .axcp-stat { background: #0D0D0D; border: 1px solid #2E2E2E; border-radius: 12px; padding: 14px 20px; flex: 1; min-width: 100px; }
       '.axcp-sv{font-size:24px;font-weight:700;color:#FF6600;line-height:1}',
       .axcp-stat-val { font-size: 26px; font-weight: 700; color: #FF6600; line-height: 1; }
       '.axcp-sl{font-size:10px;color:#555;text-transform:uppercase;letter-spacing:.08em;margin-top:4px}',
       .axcp-stat-label { font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: .08em; margin-top: 4px; }
       '.axcp-ctrl{display:flex;gap:10px;padding:12px 28px;background:#111;border-bottom:1px solid #2E2E2E;flex-wrap:wrap;align-items:center}',
       .axcp-controls { display: flex; gap: 10px; padding: 14px 28px; background: #111; border-bottom: 1px solid #2E2E2E; flex-wrap: wrap; align-items: center; }
       '.axcp-inp{flex:1;min-width:160px;background:#1A1A1A;border:1px solid #2E2E2E;border-radius:8px;padding:8px 13px;color:#fff;font-size:13px;outline:none}',
       .axcp-search { flex: 1; min-width: 180px; background: #1A1A1A; border: 1px solid #2E2E2E; border-radius: 8px; padding: 9px 14px; color: #fff; font-size: 13px; outline: none; }
       '.axcp-inp:focus{border-color:rgba(255,102,0,.5)}',
       .axcp-search::placeholder { color: #444; }
       '.axcp-sel{background:#1A1A1A;border:1px solid #2E2E2E;border-radius:8px;padding:8px 11px;color:#fff;font-size:12px;outline:none;cursor:pointer;max-width:200px}',
      .axcp-search:focus { border-color: rgba(255,102,0,.5); }
       '.axcp-sel:focus{border-color:rgba(255,102,0,.5)}',
       .axcp-select { background: #1A1A1A; border: 1px solid #2E2E2E; border-radius: 8px; padding: 9px 12px; color: #fff; font-size: 12px; outline: none; cursor: pointer; max-width: 200px; }
       '.axcp-cnt{background:rgba(255,102,0,.12);border:1px solid rgba(255,102,0,.3);color:#FF6600;border-radius:6px;padding:4px 12px;font-size:12px;font-weight:600;white-space:nowrap}',
       .axcp-select:focus { border-color: rgba(255,102,0,.5); }
       '.axcp-rbtn{background:linear-gradient(135deg,#FF6600,#FF8533);border:none;border-radius:8px;color:#fff;padding:9px 16px;font-size:12px;font-weight:700;cursor:pointer}',
       .axcp-count { background: rgba(255,102,0,.12); border: 1px solid rgba(255,102,0,.3); color: #FF6600; border-radius: 6px; padding: 4px 12px; font-size: 12px; font-weight: 600; white-space: nowrap; }
      '.axcp-rbtn:hover{opacity:.85}',
       .axcp-refresh-btn { background: linear-gradient(135deg,#FF6600,#FF8533); border: none; border-radius: 8px; color: #fff; padding: 9px 18px; font-size: 12px; font-weight: 700; cursor: pointer; white-space: nowrap; letter-spacing: .03em; }
       '.axcp-tw{overflow-x:auto;padding:0 28px 32px;background:#0D0D0D}',
      .axcp-refresh-btn:hover { opacity: .85; }
       'table.axcp-t{width:100%;border-collapse:collapse;margin-top:14px;font-size:13px}',
       .axcp-table-wrap { overflow-x: auto; padding: 0 28px 32px; background: #0D0D0D; }
       'table.axcp-t thead th{background:#111;color:#555;text-transform:uppercase;letter-spacing:.07em;font-size:10px;font-weight:600;padding:10px 12px;border-bottom:1px solid #2E2E2E;cursor:pointer;white-space:nowrap;user-select:none}',
       table.axcp-tbl { width: 100%; border-collapse: collapse; margin-top: 14px; font-size: 13px; }
       'table.axcp-t thead th:hover{color:#FF6600}',
       table.axcp-tbl thead th { background: #111; color: #555; text-transform: uppercase; letter-spacing: .08em; font-size: 10px; font-weight: 600; padding: 10px 12px; border-bottom: 1px solid #2E2E2E; cursor: pointer; white-space: nowrap; user-select: none; position: sticky; top: 0; z-index: 2; }
       'table.axcp-t thead th.sa::after{content:" u2191";color:#FF6600}',
       table.axcp-tbl thead th:hover { color: #FF6600; }
       'table.axcp-t thead th.sd::after{content:" u2193";color:#FF6600}',
       table.axcp-tbl thead th.s-asc::after { content: " "; color: #FF6600; }
       'table.axcp-t td{padding:8px 12px;border-bottom:1px solid rgba(255,255,255,.03);vertical-align:middle}',
       table.axcp-tbl thead th.s-desc::after { content: " "; color: #FF6600; }
       'table.axcp-t tr:hover td{background:rgba(255,102,0,.04)}',
       table.axcp-tbl td { padding: 8px 12px; border-bottom: 1px solid rgba(255,255,255,.03); vertical-align: middle; }
       'table.axcp-t tr:last-child td{border-bottom:none}',
       table.axcp-tbl tr:hover td { background: rgba(255,102,0,.04); }
       '.axcp-al{color:#fff;text-decoration:none;font-weight:500;font-size:13px}',
       table.axcp-tbl tr:last-child td { border-bottom: none; }
       '.axcp-al:hover{color:#FF6600}',
       .axcp-art-link { color: #fff; text-decoration: none; font-weight: 500; font-size: 13px; }
       '.axcp-cp{display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:600;white-space:nowrap}',
       .axcp-art-link:hover { color: #FF6600; }
       '.axcp-st{display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:600}',
       .axcp-cat-pill { display: inline-block; padding: 2px 9px; border-radius: 20px; font-size: 11px; font-weight: 600; white-space: nowrap; }
       '.s-stu{background:rgba(255,60,60,.15);color:#ff7070;border:1px solid rgba(255,60,60,.25)}',
       .axcp-status { display: inline-block; padding: 2px 9px; border-radius: 20px; font-size: 11px; font-weight: 600; }
       '.s-sho{background:rgba(255,166,0,.15);color:#ffb020;border:1px solid rgba(255,166,0,.25)}',
       .st-stub { background: rgba(255,60,60,.15); color: #ff7070; border: 1px solid rgba(255,60,60,.25); }
       '.s-med{background:rgba(61,220,132,.15);color:#3ddc84;border:1px solid rgba(61,220,132,.25)}',
       .st-short { background: rgba(255,166,0,.15); color: #ffb020; border: 1px solid rgba(255,166,0,.25); }
       '.s-lon{background:rgba(100,160,255,.15);color:#78b0ff;border:1px solid rgba(100,160,255,.25)}',
       .st-medium { background: rgba(61,220,132,.15); color: #3ddc84; border: 1px solid rgba(61,220,132,.25); }
       '.axcp-n{text-align:right;font-variant-numeric:tabular-nums;color:#777}',
       .st-long { background: rgba(100,160,255,.15); color: #78b0ff; border: 1px solid rgba(100,160,255,.25); }
       '.axcp-bw{background:#1A1A1A;border-radius:3px;height:5px;width:60px;display:inline-block;vertical-align:middle;margin-left:6px;overflow:hidden}',
       .axcp-num { text-align: right; font-variant-numeric: tabular-nums; color: #777; }
       '.axcp-bf{height:5px;border-radius:3px;background:linear-gradient(to right,#FF6600,#FF8533)}',
       .axcp-bar-wrap { background: #1A1A1A; border-radius: 3px; height: 5px; width: 60px; display: inline-block; vertical-align: middle; margin-left: 6px; overflow: hidden; }
       '.axcp-load{text-align:center;padding:80px 40px;color:#555;font-size:15px;background:#0D0D0D}',
       .axcp-bar-fill { height: 5px; border-radius: 3px; background: linear-gradient(to right, #FF6600, #FF8533); }
       '.axcp-prog{color:#FF6600;font-size:13px;margin-top:10px}',
       .axcp-loading { text-align: center; padding: 80px 40px; color: #555; font-size: 15px; background: #0D0D0D; }
       '.axcp-z{color:#333}',
       .axcp-progress { color: #FF6600; font-size: 13px; margin-top: 10px; }
       '.axcp-hi{color:#FF6600;font-weight:600}',
       .axcp-zero { color: #333; }
       '.axcp-wt{color:#3ddc84;font-weight:600}'
       .axcp-hi { color: #FF6600; font-weight: 600; }
     ].join('');
       .axcp-watch { color: #3ddc84; font-weight: 600; }
 
     `;
     var sEl = document.createElement('style');
     var styleEl = document.createElement('style');
     sEl.textContent = css;
     styleEl.textContent = css;
     document.head.appendChild(sEl);
     document.head.appendChild(styleEl);


    // Replace page content
     var contentEl = document.querySelector('#mw-content-text .mw-parser-output') || document.getElementById('mw-content-text');
     var contentEl = document.querySelector('#mw-content-text .mw-parser-output') || document.getElementById('mw-content-text');
     contentEl.innerHTML = '<div id="axcp-root"><div class="axcp-loading"><div style="font-size:28px;margin-bottom:16px;">⚙️</div><div>Loading article database&hellip;</div><div class="axcp-progress" id="axcp-prog">Fetching page list&hellip;</div></div></div>';
     contentEl.innerHTML = '<div id="axcp-root"><div class="axcp-load"><div style="font-size:28px;margin-bottom:14px">&#9881;</div><div>Loading article database...</div><div class="axcp-prog" id="axcp-prog">Fetching page list...</div></div></div>';


     var root = document.getElementById('axcp-root');
     var root = document.getElementById('axcp-root');
     var allData = [];
     var allData = [];
     var sortCol = 'title';
     var sortCol = 'title', sortDir = 1, filterText = '', filterCat = '', filterSt = '';
    var sortDir = 1;
    var filterText = '';
    var filterCat = '';


     var CAT_COLORS = {
     var CC = {
       'Sexual Health':           'rgba(255,100,100,.18)|#ff8080',
       'Sexual Health':                       'rgba(255,100,100,.18)|#ff8080',
       'Dating, Sex & Relationships': 'rgba(255,182,60,.18)|#ffb83c',
       'Dating, Sex & Relationships':         'rgba(255,182,60,.18)|#ffb83c',
       'Kink & BDSM':             'rgba(180,80,255,.18)|#c864ff',
       'Kink & BDSM':                         'rgba(180,80,255,.18)|#c864ff',
       'Culture, History & Politics': 'rgba(80,140,255,.18)|#6496ff',
       'Culture, History & Politics':         'rgba(80,140,255,.18)|#6496ff',
       'Fashion & Visual Signaling': 'rgba(255,220,60,.18)|#ffdc3c',
       'Fashion & Visual Signaling':         'rgba(255,220,60,.18)|#ffdc3c',
       'Community & Identity':     'rgba(61,220,132,.18)|#3ddc84',
       'Community & Identity':               'rgba(61,220,132,.18)|#3ddc84',
       'Drugs, Party Culture & Harm Reduction': 'rgba(255,120,40,.18)|#ff8028',
       'Drugs, Party Culture & Harm Reduction':'rgba(255,120,40,.18)|#ff8028',
       'Life Planning':           'rgba(80,200,255,.18)|#50c8ff'
       'Life Planning':                       'rgba(80,200,255,.18)|#50c8ff'
     };
     };
    var KCATS = Object.keys(CC);


    var KNOWN_CATS = Object.keys(CAT_COLORS);
     function catSty(cat) {
 
       var s = CC[cat] || 'rgba(120,120,120,.18)|#888', p = s.split('|');
     function catStyle(cat) {
       var s = CAT_COLORS[cat] || 'rgba(120,120,120,.18)|#888';
      var p = s.split('|');
       return 'background:'+p[0]+';color:'+p[1]+';border:1px solid '+p[1]+';';
       return 'background:'+p[0]+';color:'+p[1]+';border:1px solid '+p[1]+';';
     }
     }
 
     function wSt(w) {
     function wordStatus(w) {
       if (w < 150)  return ['stu','Stub'];
       if (w < 150)  return ['stub',   'Stub'];
       if (w < 500)  return ['sho','Short'];
       if (w < 500)  return ['short', 'Short'];
       if (w < 1500) return ['med','Medium'];
       if (w < 1500) return ['medium', 'Medium'];
       return ['lon','Long'];
       return ['long', 'Long'];
     }
     }
 
     function fDate(iso) {
     function fmtDate(iso) {
       if (!iso) return '<span class="axcp-z">-</span>';
       if (!iso) return '<span class="axcp-zero"></span>';
       var d = new Date(iso);
       var d = new Date(iso);
       return d.toLocaleDateString('en-GB', {day:'2-digit',month:'short',year:'numeric'});
       return d.toLocaleDateString('en-GB',{day:'2-digit',month:'short',year:'numeric'});
     }
     }
 
     function fKb(b) {
     function fmtKb(bytes) {
       if (!b) return '<span class="axcp-z">-</span>';
       if (!bytes) return '<span class="axcp-zero"></span>';
       return (b/1024).toFixed(1)+' <span style="color:#444;font-size:10px">KB</span>';
       return (bytes/1024).toFixed(1)+' <span style="color:#444;font-size:11px">KB</span>';
    }
    function esc(s) {
      return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
     }
     }
 
     function setP(msg) { var el = document.getElementById('axcp-prog'); if (el) el.textContent = msg; }
     function esc(s) { return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }


     function render(data) {
     function render(data) {
       var filtered = data.filter(function(r) {
       var fd = data.filter(function(r) {
         var mt = !filterText || r.title.toLowerCase().indexOf(filterText) >= 0 || (r.cat||'').toLowerCase().indexOf(filterText) >= 0 || (r.subcat||'').toLowerCase().indexOf(filterText) >= 0;
         var mt = !filterText || r.title.toLowerCase().indexOf(filterText)>=0 || (r.cat||'').toLowerCase().indexOf(filterText)>=0;
         var mc = !filterCat || r.cat === filterCat;
         var mc = !filterCat || r.cat === filterCat;
         return mt && mc;
        var ms = !filterSt || wSt(r.words||0)[0] === filterSt;
         return mt && mc && ms;
       });
       });
      fd.sort(function(a,b) {
        var av=a[sortCol]||0, bv=b[sortCol]||0;
        if (typeof av==='number'&&typeof bv==='number') return sortDir*(av-bv);
        return sortDir*String(av).localeCompare(String(bv));
      });
      var mxW = Math.max.apply(null,[1].concat(fd.map(function(r){return r.words||0;})));
      var totW = fd.reduce(function(s,r){return s+(r.words||0);},0);
      var avgW = fd.length ? Math.round(totW/fd.length) : 0;
      var totR = fd.reduce(function(s,r){return s+(r.revisions||0);},0);
      var nocat = fd.filter(function(r){return !r.cat;}).length;
      var cats={};
      data.forEach(function(r){if(r.cat)cats[r.cat]=1;});
      var catList=Object.keys(cats).sort();
      var h='';
      h+='<div class="axcp-hdr">';
      h+='<div class="axcp-ttl">&#9881; Admin Control Panel</div>';
      h+='<div class="axcp-sub">AlphaX Wiki &bull; Article Database &bull; Live Data</div>';
      h+='<div class="axcp-sr">';
      h+='<div class="axcp-sc"><div class="axcp-sv">'+fd.length+'</div><div class="axcp-sl">Articles</div></div>';
      h+='<div class="axcp-sc"><div class="axcp-sv">'+avgW.toLocaleString()+'</div><div class="axcp-sl">Avg Words</div></div>';
      h+='<div class="axcp-sc"><div class="axcp-sv">'+totW.toLocaleString()+'</div><div class="axcp-sl">Total Words</div></div>';
      h+='<div class="axcp-sc"><div class="axcp-sv">'+totR.toLocaleString()+'</div><div class="axcp-sl">Total Revisions</div></div>';
      h+='<div class="axcp-sc"><div class="axcp-sv">'+catList.length+'</div><div class="axcp-sl">Categories</div></div>';
      h+='<div class="axcp-sc"><div class="axcp-sv" style="color:'+(nocat>0?'#ff7070':'#3ddc84')+'">'+nocat+'</div><div class="axcp-sl">Uncategorised</div></div>';
      h+='</div></div>';
      h+='<div class="axcp-ctrl">';
      h+='<input class="axcp-inp" id="axcps" type="text" placeholder="Search articles or categories..." value="'+esc(filterText)+'">';
      h+='<select class="axcp-sel" id="axcpc"><option value="">All Categories</option>';
      catList.forEach(function(c){h+='<option value="'+esc(c)+'"'+(filterCat===c?' selected':'')+'>'+esc(c)+'</option>';});
      h+='</select>';
      h+='<select class="axcp-sel" id="axcpf"><option value="">All Statuses</option>';
      [['stu','Stub'],['sho','Short'],['med','Medium'],['lon','Long']].forEach(function(s){
        h+='<option value="'+s[0]+'"'+(filterSt===s[0]?' selected':'')+'>'+s[1]+'</option>';
      });
      h+='</select>';
      h+='<span class="axcp-cnt">'+fd.length+' articles</span>';
      h+='<button class="axcp-rbtn" id="axcpr">&#8635; Refresh Data</button>';
      h+='</div>';
      h+='<div class="axcp-tw"><table class="axcp-t"><thead><tr>';
      [{k:'title',l:'Article'},{k:'cat',l:'Category'},{k:'subcat',l:'Subcategory'},
      {k:'words',l:'Words'},{k:'size',l:'Size (KB)'},{k:'status',l:'Status'},
      {k:'touched',l:'Last Edited'},{k:'revisions',l:'Revisions'},
      {k:'watchers',l:'Watchers'},{k:'links',l:'Inbound Links'}
      ].forEach(function(c){
        var cl=sortCol===c.k?(sortDir===1?'sa':'sd'):'';
        h+='<th class="'+cl+'" data-col="'+c.k+'">'+c.l+'</th>';
      });
      h+='</tr></thead><tbody>';
      if (!fd.length) {
        h+='<tr><td colspan="10" style="text-align:center;padding:50px;color:#333">No articles match your filters.</td></tr>';
      }
      fd.forEach(function(r){
        var st=wSt(r.words||0);
        var bp=mxW>0?Math.round(((r.words||0)/mxW)*60):0;
        h+='<tr>';
        h+='<td><a class="axcp-al" href="/wiki/'+encodeURIComponent((r.title||'').replace(/ /g,'_'))+'" target="_blank">'+esc(r.title)+'</a></td>';
        h+='<td>'+(r.cat?'<span class="axcp-cp" style="'+catSty(r.cat)+'">'+esc(r.cat)+'</span>':'<span class="axcp-z">-</span>')+'</td>';
        h+='<td style="color:#555;font-size:12px">'+esc(r.subcat||'-')+'</td>';
        h+='<td class="axcp-n">'+(r.words||0).toLocaleString()+'<span class="axcp-bw"><span class="axcp-bf" style="width:'+bp+'px"></span></span></td>';
        h+='<td class="axcp-n">'+fKb(r.size)+'</td>';
        h+='<td><span class="axcp-st s-'+st[0]+'">'+st[1]+'</span></td>';
        h+='<td style="color:#555;white-space:nowrap;font-size:12px">'+fDate(r.touched)+'</td>';
        h+='<td class="axcp-n '+(r.revisions>20?'axcp-hi':'')+'">'+(r.revisions||0)+'</td>';
        h+='<td class="axcp-n '+(r.watchers>0?'axcp-wt':'axcp-z')+'">'+(r.watchers>0?'&#128065; '+r.watchers:'-')+'</td>';
        h+='<td class="axcp-n '+(r.links>5?'axcp-hi':'')+'">'+(r.links||0)+'</td>';
        h+='</tr>';
      });
      h+='</tbody></table></div>';
      root.innerHTML=h;


       filtered.sort(function(a,b) {
       var si=document.getElementById('axcps');
        var av = a[sortCol], bv = b[sortCol];
      if(si)si.addEventListener('input',function(){filterText=this.value.toLowerCase();render(allData);});
        if (typeof av === 'number' && typeof bv === 'number') return sortDir*(av-bv);
      var ci=document.getElementById('axcpc');
         return sortDir*String(av||'').localeCompare(String(bv||''));
      if(ci)ci.addEventListener('change',function(){filterCat=this.value;render(allData);});
      var fi=document.getElementById('axcpf');
      if(fi)fi.addEventListener('change',function(){filterSt=this.value;render(allData);});
      var ri=document.getElementById('axcpr');
      if(ri)ri.addEventListener('click',function(){allData=[];loadData();});
      document.querySelectorAll('table.axcp-t thead th').forEach(function(th){
         th.addEventListener('click',function(){
          var col=this.getAttribute('data-col');
          if(sortCol===col){sortDir*=-1;}else{sortCol=col;sortDir=1;}
          render(allData);
        });
       });
       });
    }
    function apiFetch(url) {
      return fetch(url).then(function(r){return r.json();});
    }


      var maxW = Math.max.apply(null, [1].concat(filtered.map(function(r){return r.words||0;})));
    function loadData() {
      var totalW = filtered.reduce(function(s,r){return s+(r.words||0);},0);
      root.innerHTML='<div class="axcp-load"><div style="font-size:28px;margin-bottom:14px">&#9881;</div><div>Loading article database...</div><div class="axcp-prog" id="axcp-prog">Step 1/4 -- Fetching page list...</div></div>';
      var avgW  = filtered.length ? Math.round(totalW/filtered.length) : 0;
       var pages=[], map={}, ids=[];
       var totalR = filtered.reduce(function(s,r){return s+(r.revisions||0);},0);
      var nocat  = filtered.filter(function(r){return !r.cat;}).length;


       // Build category list
       function fetchPages(apc) {
      var cats = {};
        var u='/api.php?action=query&list=allpages&apnamespace=0&aplimit=500&format=json';
      data.forEach(function(r){ if(r.cat) cats[r.cat]=1; });
        if(apc) u+='&apcontinue='+encodeURIComponent(apc);
      var catList = Object.keys(cats).sort();
        return apiFetch(u).then(function(d){
          if(!d||!d.query)return;
          pages=pages.concat(d.query.allpages||[]);
          var cont=d.continue&&d.continue.apcontinue;
          if(cont)return fetchPages(cont);
        });
      }


       var h = '';
       // Step 2: info + categories (no revision params to avoid conflicts)
      function fetchInfoChunk(i) {
        if(i>=ids.length) return Promise.resolve();
        var chunk=ids.slice(i,i+50).join('|');
        return apiFetch('/api.php?action=query&pageids='+chunk+'&prop=info|categories&inprop=watchers&cllimit=15&format=json').then(function(d){
          if(!d||!d.query||!d.query.pages){return fetchInfoChunk(i+50);}
          Object.keys(d.query.pages).forEach(function(pid){
            var pg=d.query.pages[pid]; if(!map[pid])return;
            map[pid].size=pg.length||0;
            map[pid].touched=pg.touched||'';
            map[pid].watchers=pg.watchers!=null?Number(pg.watchers):0;
            if(pg.categories){pg.categories.forEach(function(c){
              var cn=c.title.replace('Category:','');
              if(KCATS.indexOf(cn)>=0){if(!map[pid].cat)map[pid].cat=cn;}
              else if(cn.length<60&&cn.toLowerCase().indexOf('stub')<0){if(!map[pid].subcat)map[pid].subcat=cn;}
            });}
          });
          return fetchInfoChunk(i+50);
        });
      }


       // Header
       // Step 3: revision counts
       h += '<div class="axcp-header">';
       function fetchRevChunk(i) {
      h += '<div class="axcp-title">⚙️ Admin Control Panel</div>';
        if(i>=ids.length) return Promise.resolve();
      h += '<div class="axcp-subtitle">AlphaX Wiki · Article Database · Live Data</div>';
        var chunk=ids.slice(i,i+20).join('|');
      h += '<div class="axcp-stats-row">';
        return apiFetch('/api.php?action=query&pageids='+chunk+'&prop=revisions&rvprop=ids&rvlimit=max&format=json').then(function(d){
      h += '<div class="axcp-stat"><div class="axcp-stat-val">'+filtered.length+'</div><div class="axcp-stat-label">Articles</div></div>';
          if(!d||!d.query||!d.query.pages){return fetchRevChunk(i+20);}
      h += '<div class="axcp-stat"><div class="axcp-stat-val">'+avgW.toLocaleString()+'</div><div class="axcp-stat-label">Avg Words</div></div>';
          Object.keys(d.query.pages).forEach(function(pid){
      h += '<div class="axcp-stat"><div class="axcp-stat-val">'+totalW.toLocaleString()+'</div><div class="axcp-stat-label">Total Words</div></div>';
            var pg=d.query.pages[pid]; if(!map[pid])return;
      h += '<div class="axcp-stat"><div class="axcp-stat-val">'+totalR.toLocaleString()+'</div><div class="axcp-stat-label">Total Revisions</div></div>';
            map[pid].revisions=(pg.revisions||[]).length;
      h += '<div class="axcp-stat"><div class="axcp-stat-val">'+catList.length+'</div><div class="axcp-stat-label">Categories</div></div>';
          });
      h += '<div class="axcp-stat"><div class="axcp-stat-val" style="color:'+(nocat>0?'#ff7070':'#3ddc84')+'">'+nocat+'</div><div class="axcp-stat-label">Uncategorised</div></div>';
          return fetchRevChunk(i+20);
       h += '</div></div>';
        });
       }


       // Controls
       // Step 4: word counts + inbound links
       h += '<div class="axcp-controls">';
       function fetchContentChunk(i) {
      h += '<input class="axcp-search" id="axcp-s" type="text" placeholder="🔍 Search articles, categories…" value="'+esc(filterText)+'">';
        if(i>=ids.length) return Promise.resolve();
      h += '<select class="axcp-select" id="axcp-cf"><option value="">All Categories ('+catList.length+')</option>';
        var chunk=ids.slice(i,i+8).join('|');
      catList.forEach(function(c){ h+='<option value="'+esc(c)+'"'+(filterCat===c?' selected':'')+'>'+esc(c)+'</option>'; });
        return apiFetch('/api.php?action=query&pageids='+chunk+'&prop=revisions|linkshere&rvprop=content&rvslots=main&lhnamespace=0&lhlimit=max&format=json').then(function(d){
      h += '</select>';
          if(!d||!d.query||!d.query.pages){return fetchContentChunk(i+8);}
      h += '<select class="axcp-select" id="axcp-sf"><option value="">All Statuses</option><option value="stub">Stub</option><option value="short">Short</option><option value="medium">Medium</option><option value="long">Long</option></select>';
          Object.keys(d.query.pages).forEach(function(pid){
      h += '<span class="axcp-count">'+filtered.length+' articles</span>';
            var pg=d.query.pages[pid]; if(!map[pid])return;
      h += '<button class="axcp-refresh-btn" id="axcp-reload">⟳ Refresh</button>';
            map[pid].links=(pg.linkshere||[]).length;
      h += '</div>';
            if(!pg.revisions||!pg.revisions[0])return;
            if(!pg.revisions||!pg.revisions[0])return;
            var slot=pg.revisions[0].slots?pg.revisions[0].slots.main:pg.revisions[0];
            var raw=slot['*']||slot.content||'';
            // Remove template names but keep parameter text
            var c=raw.replace(/\{\{[A-Za-z][^|\}]*\|?/g,'').replace(/\}\}/g,' ');
            c=c.replace(/\[\[File:[^\]]+\]\]/gi,' ');
            c=c.replace(/\[\[(?:[^\]|]+\|)?([^\]]+)\]\]/g,'$1');
            c=c.replace(/<[^>]+>/g,' ').replace(/<!--[^>]*-->/g,' ');
            c=c.replace(/={2,}[^=]+=={2,}/g,' ');
            c=c.replace(/[|!=*#;:{}\/\[\]]/g,' ');
            c=c.replace(/\s+/g,' ').trim();
            map[pid].words=c?c.split(/\s+/).filter(function(w){return w.length>2;}).length:0;
          });
          return fetchContentChunk(i+8);
        });
      }


       // Table
       fetchPages(null).then(function(){
      h += '<div class="axcp-table-wrap"><table class="axcp-tbl">';
        setP('Step 2/4 -- Fetching page info & categories for '+pages.length+' articles...');
      h += '<thead><tr>';
        pages.forEach(function(p){map[p.pageid]={title:p.title,cat:'',subcat:'',words:0,size:0,touched:'',revisions:0,watchers:0,links:0};});
      [
         ids=pages.map(function(p){return p.pageid;});
        {k:'title',l:'Article Title'},
         return fetchInfoChunk(0);
        {k:'cat',l:'Category'},
      }).then(function(){
        {k:'subcat',l:'Subcategory'},
         setP('Step 3/4 -- Counting revisions...');
        {k:'words',l:'Words'},
         return fetchRevChunk(0);
        {k:'size',l:'Size'},
      }).then(function(){
        {k:'status',l:'Status'},
         setP('Step 4/4 -- Calculating word counts & inbound links...');
         {k:'created',l:'Published'},
        return fetchContentChunk(0);
         {k:'touched',l:'Last Edited'},
       }).then(function(){
         {k:'revisions',l:'Revisions'},
         allData=Object.values(map);
         {k:'watchers',l:'Watchers'},
        render(allData);
         {k:'links',l:'Inbound Links'}
      }).catch(function(e){
       ].forEach(function(c) {
         root.innerHTML='<div class="axcp-load" style="color:#ff7070">&#9888; Error: '+e.message+'</div>';
         var cls = sortCol===c.k ? (sortDir===1?'s-asc':'s-desc') : '';
         h += '<th class="'+cls+'" data-col="'+c.k+'">'+c.l+'</th>';
       });
       });
      h += '</tr></thead><tbody>';
    }


       if (filtered.length === 0) {
    loadData();
         h += '<tr><td colspan="11" style="text-align:center;padding:50px;color:#444;">No articles match your filters.</td></tr>';
  });
})();
 
 
// Category Grid Component - background images for ax-cat-grid
(function() {
  // Map of card ID -> background image URL and fallback gradient
  var catImages = {
    'ax-cat-img-1': { url: 'https://alphax.wiki/images/4/43/Sexual_Health_Hero.jpg', pos: 'center center' },
    'ax-cat-img-2': { url: 'https://alphax.wiki/images/e/e5/Kink_BDSM_Hero.png', pos: 'center center' },
    'ax-cat-img-3': { gradient: 'linear-gradient(135deg, #0D1B2A 0%, #1B3A4B 40%, #0D2B3E 100%)' },
    'ax-cat-img-4': { gradient: 'linear-gradient(135deg, #1A0D0D 0%, #3A1A0D 50%, #2B1A0D 100%)' },
    'ax-cat-img-5': { url: 'https://alphax.wiki/images/0/0b/Dating_Sex_Relationships_Hero.png', pos: 'center 30%' },
    'ax-cat-img-6': { gradient: 'linear-gradient(135deg, #1A0D1A 0%, #2E0D3A 50%, #1A0D2B 100%)' },
    'ax-cat-img-7': { url: 'https://alphax.wiki/images/2/25/Culture_History_Politics_Hero.png', pos: 'center center' },
    'ax-cat-img-8': { gradient: 'linear-gradient(135deg, #0D1A0D 0%, #0D2B1A 50%, #0D1A2B 100%)' }
  };
 
  function applyCatImages() {
    Object.keys(catImages).forEach(function(id) {
      var el = document.getElementById(id);
       if (!el) return;
      var cfg = catImages[id];
      if (cfg.url) {
         el.style.backgroundImage = 'url("' + cfg.url + '")';
        el.style.backgroundSize = 'cover';
        el.style.backgroundPosition = cfg.pos || 'center center';
      } else if (cfg.gradient) {
        el.style.backgroundImage = cfg.gradient;
       }
       }
    });
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', applyCatImages);
  } else {
    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();
  }
})();


      filtered.forEach(function(r) {
        var st = wordStatus(r.words||0);
        var barPct = maxW > 0 ? Math.round(((r.words||0)/maxW)*60) : 0;
        h += '<tr>';
        h += '<td><a class="axcp-art-link" href="/wiki/'+encodeURIComponent((r.title||'').replace(/ /g,'_'))+'" target="_blank">'+esc(r.title||'')+'</a></td>';
        h += '<td>'+(r.cat ? '<span class="axcp-cat-pill" style="'+catStyle(r.cat)+'">'+esc(r.cat)+'</span>' : '<span class="axcp-zero">—</span>')+'</td>';
        h += '<td style="color:#666;font-size:12px;">'+esc(r.subcat||'—')+'</td>';
        h += '<td class="axcp-num">'+(r.words||0).toLocaleString()+'<span class="axcp-bar-wrap"><span class="axcp-bar-fill" style="width:'+barPct+'px"></span></span></td>';
        h += '<td class="axcp-num">'+fmtKb(r.size)+'</td>';
        h += '<td><span class="axcp-status st-'+st[0]+'">'+st[1]+'</span></td>';
        h += '<td style="color:#666;white-space:nowrap;font-size:12px;">'+fmtDate(r.created)+'</td>';
        h += '<td style="color:#666;white-space:nowrap;font-size:12px;">'+fmtDate(r.touched)+'</td>';
        h += '<td class="axcp-num '+(r.revisions>20?'axcp-hi':'')+'">'+((r.revisions||0))+'</td>';
        h += '<td class="axcp-num '+(r.watchers>0?'axcp-watch':'axcp-zero')+'">'+((r.watchers!=null&&r.watchers>0)? '👁 '+r.watchers : '0')+'</td>';
        h += '<td class="axcp-num '+(r.links>5?'axcp-hi':'')+'">'+((r.links||0))+'</td>';
        h += '</tr>';
      });


      h += '</tbody></table></div>';
      root.innerHTML = h;


      // Bind events
// ===== Permanent Favicon =====
      var searchEl = document.getElementById('axcp-s');
(function() {
      if (searchEl) searchEl.addEventListener('input', function(){ filterText = this.value.toLowerCase(); render(allData); });
  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">';


      var catEl = document.getElementById('axcp-cf');
    // Stats cards
      if (catEl) catEl.addEventListener('change', function(){ filterCat = this.value; render(allData); });
    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>';


       var sfEl = document.getElementById('axcp-sf');
    if (sorted.length === 0) {
       if (sfEl) sfEl.addEventListener('change', function() {
       html += '<div class="cuc-no-data">';
        var sv = this.value;
       html += '<strong>📍 No country data available yet.</strong><br>';
        if (!sv) { render(allData); return; }
      html += 'This counter tracks users by country using the <strong>Babel extension</strong> language categories.<br>';
        // Filter by status
      html += 'Users can declare their language/country by adding babel tags to their user page:<br><br>';
        var prev = allData;
      html += '<code>{{#babel:en|de|fr}}</code><br><br>';
        var tmp = allData.filter(function(r){ return wordStatus(r.words||0)[0] === sv; });
      html += 'Once users add babel tags, this counter will automatically display their country distribution.<br>';
        allData = prev;
      html += 'Currently tracking <strong>' + totalUsers + ' registered users</strong> on this wiki.';
        // Temporary local render
      html += '</div>';
         var savedFilter = filterText;
    } else {
         filterText = '';
      var maxCount = langCounts[sorted[0]] || 1;
         var fc = filterCat;
      html += '<table id="cuc-table"><thead><tr>';
         filterCat = '';
      html += '<th style="width:40px">#</th>';
         // just re-render with status override
      html += '<th>Country / Region</th>';
        root.querySelector('#axcp-reload') && void 0;
      html += '<th style="width:70px;text-align:center">Users</th>';
         render(tmp);
      html += '<th style="width:200px">Distribution</th>';
         allData = prev;
      html += '</tr></thead><tbody>';
         filterText = savedFilter;
      sorted.forEach(function(lang, idx) {
         filterCat = fc;
         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>';
    }


      var reloadEl = document.getElementById('axcp-reload');
    html += '<div id="cuc-timestamp">Last updated: ' + new Date().toLocaleString() + '</div>';
      if (reloadEl) reloadEl.addEventListener('click', function(){ allData=[]; loadData(); });
    html += '</div>';


      document.querySelectorAll('table.axcp-tbl thead th').forEach(function(th){
    container.innerHTML = html;
        th.addEventListener('click', function(){
  }
          var col = this.getAttribute('data-col');
 
          if (sortCol === col) { sortDir *= -1; } else { sortCol = col; sortDir = 1; }
  function buildCounter() {
          render(allData);
    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 setProgress(msg) {
     function fetchLangCat(lang) {
       var el = document.getElementById('axcp-prog');
       return $.getJSON(mw.util.wikiScript('api'), {
       if (el) el.textContent = msg;
        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;
  }


    async function loadData() {
  function buildTable() {
      root.innerHTML = '<div class="axcp-loading"><div style="font-size:28px;margin-bottom:16px;">⚙️</div><div>Loading article database&hellip;</div><div class="axcp-progress" id="axcp-prog">Step 1/4 — Fetching page list…</div></div>';
    if (statusSpan) statusSpan.textContent = 'Fetching categories…';


      try {
    $.getJSON(apiBase, { action:'query', list:'allcategories', aclimit:500, format:'json' })
        // 1. Get all page IDs
      .done(function(data) {
        var pages = [];
        var topCats = (data.query && data.query.allcategories) ? data.query.allcategories.map(function(c){ return c['*']; }) : [];
        var apcontinue = null;
        var rows = [];
        do {
        var totalCats = topCats.length;
          var u = '/api.php?action=query&list=allpages&apnamespace=0&aplimit=500&format=json';
         var processed = 0;
          if (apcontinue) u += '&apcontinue=' + encodeURIComponent(apcontinue);
          var r = await fetch(u); var d = await r.json();
          pages = pages.concat(d.query.allpages);
          apcontinue = d.continue ? d.continue.apcontinue : null;
         } while (apcontinue);


         setProgress('Step 2/4 — Fetching page info & categories for '+pages.length+' articles…');
         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 map = {};
            var subTotal = pages.length + subcats.length;
        pages.forEach(function(p){ map[p.pageid] = {title:p.title,cat:'',subcat:'',words:0,size:0,created:'',touched:'',revisions:0,watchers:null,links:0,status:''}; });
            var subProcessed = 0;
        var ids = pages.map(function(p){return p.pageid;});


        // 2. Info + categories + first revision (creation date)
            function subDone() {
        for (var i=0; i<ids.length; i+=50) {
              subProcessed++;
          var chunk = ids.slice(i,i+50).join('|');
              if (subProcessed >= subTotal) checkDone();
          var r2 = await fetch('/api.php?action=query&pageids='+chunk+'&prop=info|categories|revisions&inprop=watchers&rvprop=timestamp&rvdir=newer&rvlimit=1&cllimit=15&format=json');
            }
          var d2 = await r2.json();
 
          Object.keys(d2.query.pages).forEach(function(pid){
            pages.forEach(function(page) {
            var pg = d2.query.pages[pid]; if(!map[pid])return;
              rows.push({ category: catName, sub: '—', title: page.title, words: 'n/a', focus: catName });
            map[pid].size    = pg.length || 0;
              subDone();
            map[pid].touched = pg.touched || '';
             });
            map[pid].watchers = (pg.watchers != null) ? pg.watchers : null;
 
             if (pg.revisions && pg.revisions[0]) map[pid].created = pg.revisions[0].timestamp || '';
             subcats.forEach(function(subcat) {
             if (pg.categories) {
              var subName = subcat.title.replace('Category:','');
              pg.categories.forEach(function(c){
              getCategoryMembers(subName, 'page', null, [], function(subPages) {
                var cn = c.title.replace('Category:','');
                subPages.filter(function(m){ return m.ns === 0; }).forEach(function(page) {
                if (KNOWN_CATS.indexOf(cn) >= 0) { if(!map[pid].cat) map[pid].cat = cn; }
                  rows.push({ category: catName, sub: subName, title: page.title, words: 'n/a', focus: catName });
                else if (cn.indexOf('stub')<0 && cn.indexOf('article')<0 && cn.length < 60) { if(!map[pid].subcat) map[pid].subcat = cn; }
                });
                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 || []; });
  }


        setProgress('Step 3/4 — Counting revisions…');
  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 || []; });
  }


        // 3. Revision counts (rvlimit=max gives us up to 500; count them)
  function fetchBabel(username) {
        for (var j=0; j<ids.length; j+=20) {
    return fetch(API + '?action=query&titles=User:' + encodeURIComponent(username) + '&prop=categories&format=json')
          var chunk2 = ids.slice(j,j+20).join('|');
      .then(function(r) { return r.json(); })
          var r3 = await fetch('/api.php?action=query&pageids='+chunk2+'&prop=revisions&rvprop=ids&rvlimit=max&format=json');
      .then(function(d) {
          var d3 = await r3.json();
        var pages = (d.query && d.query.pages) ? d.query.pages : {};
          Object.keys(d3.query.pages).forEach(function(pid){
        var langs = [];
             var pg = d3.query.pages[pid]; if(!map[pid])return;
        Object.values(pages).forEach(function(p) {
             map[pid].revisions = (pg.revisions||[]).length;
          (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);
    });
  }


        setProgress('Step 4/4 — Calculating word counts & inbound links…');
  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;
  }


        // 4. Word counts from wikitext + inbound links
  function renderTable(rows, total, period) {
        for (var k=0; k<ids.length; k+=10) {
    var tbody = document.getElementById('geo-table-body');
          var chunk3 = ids.slice(k,k+10).join('|');
    var titleEl = document.getElementById('geo-table-title');
          var r4 = await fetch('/api.php?action=query&pageids='+chunk3+'&prop=revisions|linkshere&rvprop=content&rvslots=main&lhnamespace=0&lhlimit=max&format=json');
    if (!tbody) return;
          var d4 = await r4.json();
    var labels = {day:'Today', week:'This Week', month:'This Month', all:'All Time'};
          Object.keys(d4.query.pages).forEach(function(pid){
    if (titleEl) titleEl.textContent = 'Users by Country – ' + (labels[period]||'All Time');
            var pg = d4.query.pages[pid]; if(!map[pid])return;
    if (!rows.length) {
            map[pid].links = (pg.linkshere||[]).length;
      tbody.innerHTML = '<tr><td colspan="5" style="padding:20px;text-align:center;color:#555;">No activity for this period.</td></tr>';
            if (!pg.revisions||!pg.revisions[0]) return;
      return;
            var slot = pg.revisions[0].slots ? pg.revisions[0].slots.main : pg.revisions[0];
    }
            var content = slot['*'] || slot.content || '';
    var html = '';
            var clean = content
    rows.forEach(function(row, i) {
              .replace(/<!--[sS]*?-->/g,'')
      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>';
              .replace(/<[^>]+>/g,' ')
      html += '<tr style="background:' + (i%2===0?'#1a1a1a':'#1e1e1e') + ';border-top:1px solid #222;">' +
              .replace(/[[File:[^]]+]]/gi,'')
        '<td style="padding:10px 16px;color:#666;">' + (i+1) + '</td>' +
              .replace(/[[(?:[^]|]+|)?([^]]+)]]/g,'$1')
        '<td style="padding:10px 16px;">' + (COUNTRY_MAP[row.lang]||('🌐 '+row.lang)) + '</td>' +
              .replace(/{{[sS]*?}}/g,'')
        '<td style="padding:10px 16px;text-align:right;font-weight:bold;color:#ff6600;">' + row.users + '</td>' +
              .replace(/={2,}([^=]+)={2,}/g,'$1')
        '<td style="padding:10px 16px;">' + bar + '</td>' +
              .replace(/[|!*#;:]/g,' ')
        '<td style="padding:10px 16px;text-align:right;color:#aaa;">' + row.edits + '</td></tr>';
              .replace(/https?://S+/g,'')
    });
              .replace(/s+/g,' ').trim();
    tbody.innerHTML = html;
            map[pid].words = clean ? clean.split(/s+/).filter(function(w){return w.length>1;}).length : 0;
  }
          });
        }


        allData = Object.values(map);
  var cachedUsers = null, cachedChanges = null, cachedBabel = {}, currentPeriod = 'day';
        render(allData);


       } catch(e) {
  function showPeriod(period) {
         root.innerHTML = '<div class="axcp-loading" style="color:#ff7070;">⚠️ Error: '+e.message+'<br><br><button onclick="location.reload()" style="background:#FF6600;border:none;color:#fff;padding:10px 20px;border-radius:8px;cursor:pointer;font-size:14px;">Retry</button></div>';
    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); });
  }


     loadData();
  if (document.readyState === 'loading') {
   });
     document.addEventListener('DOMContentLoaded', boot);
   } else {
    setTimeout(boot, 100);
  }
})();
})();