|
|
| Line 80: |
Line 80: |
|
| |
|
| == Users by Country == | | == Users by Country == |
| <div id="geo-dashboard" style="font-family:sans-serif; background:#111; border:1px solid #333; border-radius:8px; padding:20px; color:#eee;"> | | <div id="geo-dashboard" style="font-family:sans-serif;background:#111;border:1px solid #333;border-radius:8px;padding:20px;color:#eee;"> |
|
| |
|
| <div style="display:flex; gap:16px; flex-wrap:wrap; margin-bottom:20px;"> | | <div style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:20px;"> |
| <div style="flex:1; min-width:140px; background:#1a1a1a; border:1px solid #ff6600; border-radius:6px; padding:16px; text-align:center;">
| | <div style="flex:1;min-width:130px;background:#1a1a1a;border:1px solid #ff6600;border-radius:6px;padding:16px;text-align:center;"> |
| <div id="geo-total" style="font-size:2em; font-weight:bold; color:#ff6600;">–</div>
| | <div id="geo-total" style="font-size:2em;font-weight:bold;color:#ff6600;">–</div> |
| <div style="color:#aaa; font-size:0.85em; margin-top:4px;">Total Users</div>
| | <div style="color:#aaa;font-size:0.85em;margin-top:4px;">Total Users</div> |
| </div>
| |
| <div style="flex:1; min-width:140px; background:#1a1a1a; border:1px solid #444; border-radius:6px; padding:16px; text-align:center;">
| |
| <div id="geo-countries" style="font-size:2em; font-weight:bold; color:#ff6600;">–</div>
| |
| <div style="color:#aaa; font-size:0.85em; margin-top:4px;">Countries / Regions</div>
| |
| </div>
| |
| <div style="flex:1; min-width:140px; background:#1a1a1a; border:1px solid #444; border-radius:6px; padding:16px; text-align:center;">
| |
| <div id="geo-active-day" style="font-size:2em; font-weight:bold; color:#ff6600;">–</div>
| |
| <div style="color:#aaa; font-size:0.85em; margin-top:4px;">Active Today</div>
| |
| </div>
| |
| <div style="flex:1; min-width:140px; background:#1a1a1a; border:1px solid #444; border-radius:6px; padding:16px; text-align:center;">
| |
| <div id="geo-active-week" style="font-size:2em; font-weight:bold; color:#ff6600;">–</div>
| |
| <div style="color:#aaa; font-size:0.85em; margin-top:4px;">Active This Week</div>
| |
| </div>
| |
| <div style="flex:1; min-width:140px; background:#1a1a1a; border:1px solid #444; border-radius:6px; padding:16px; text-align:center;">
| |
| <div id="geo-active-month" style="font-size:2em; font-weight:bold; color:#ff6600;">–</div>
| |
| <div style="color:#aaa; font-size:0.85em; margin-top:4px;">Active This Month</div>
| |
| </div>
| |
| </div> | | </div> |
| | | <div style="flex:1;min-width:130px;background:#1a1a1a;border:1px solid #444;border-radius:6px;padding:16px;text-align:center;"> |
| <div style="display:flex; gap:12px; margin-bottom:16px; flex-wrap:wrap;"> | | <div id="geo-countries" style="font-size:2em;font-weight:bold;color:#ff6600;">–</div> |
| <button id="geo-btn-day" onclick="geoShowPeriod('day')" style="background:#ff6600; color:#fff; border:none; padding:8px 18px; border-radius:4px; cursor:pointer; font-weight:bold;">Today</button>
| | <div style="color:#aaa;font-size:0.85em;margin-top:4px;">Countries</div> |
| <button id="geo-btn-week" onclick="geoShowPeriod('week')" style="background:#333; color:#eee; border:1px solid #555; padding:8px 18px; border-radius:4px; cursor:pointer;">This Week</button>
| | </div> |
| <button id="geo-btn-month" onclick="geoShowPeriod('month')" style="background:#333; color:#eee; border:1px solid #555; padding:8px 18px; border-radius:4px; cursor:pointer;">This Month</button>
| | <div style="flex:1;min-width:130px;background:#1a1a1a;border:1px solid #444;border-radius:6px;padding:16px;text-align:center;"> |
| <button id="geo-btn-all" onclick="geoShowPeriod('all')" style="background:#333; color:#eee; border:1px solid #555; padding:8px 18px; border-radius:4px; cursor:pointer;">All Time</button>
| | <div id="geo-active-day" style="font-size:2em;font-weight:bold;color:#ff6600;">–</div> |
| <span style="margin-left:auto; color:#666; font-size:0.8em; align-self:center;" id="geo-updated"></span>
| | <div style="color:#aaa;font-size:0.85em;margin-top:4px;">Active Today</div> |
| | </div> |
| | <div style="flex:1;min-width:130px;background:#1a1a1a;border:1px solid #444;border-radius:6px;padding:16px;text-align:center;"> |
| | <div id="geo-active-week" style="font-size:2em;font-weight:bold;color:#ff6600;">–</div> |
| | <div style="color:#aaa;font-size:0.85em;margin-top:4px;">Active This Week</div> |
| | </div> |
| | <div style="flex:1;min-width:130px;background:#1a1a1a;border:1px solid #444;border-radius:6px;padding:16px;text-align:center;"> |
| | <div id="geo-active-month" style="font-size:2em;font-weight:bold;color:#ff6600;">–</div> |
| | <div style="color:#aaa;font-size:0.85em;margin-top:4px;">Active This Month</div> |
| </div> | | </div> |
|
| |
| <div id="geo-map-placeholder" style="background:#1a1a1a; border:1px solid #333; border-radius:6px; padding:12px; margin-bottom:16px; min-height:220px; position:relative; overflow:hidden;">
| |
| <svg id="geo-world-map" viewBox="0 0 900 440" style="width:100%; height:auto; display:block;"></svg>
| |
| </div> | | </div> |
|
| |
|
| <div id="geo-table-wrap" style="background:#1a1a1a; border:1px solid #333; border-radius:6px; overflow:hidden;"> | | <div style="display:flex;gap:10px;margin-bottom:16px;flex-wrap:wrap;align-items:center;"> |
| <div style="padding:12px 16px; border-bottom:1px solid #333; font-weight:bold; color:#ff6600; font-size:0.95em;" id="geo-table-title">Users by Country</div>
| | <button id="geo-btn-day" style="background:#ff6600;color:#fff;border:none;padding:8px 18px;border-radius:4px;cursor:pointer;font-weight:bold;">Today</button> |
| <table style="width:100%; border-collapse:collapse; font-size:0.9em;">
| | <button id="geo-btn-week" style="background:#333;color:#eee;border:1px solid #555;padding:8px 18px;border-radius:4px;cursor:pointer;">This Week</button> |
| <thead>
| | <button id="geo-btn-month" style="background:#333;color:#eee;border:1px solid #555;padding:8px 18px;border-radius:4px;cursor:pointer;">This Month</button> |
| <tr style="background:#222; color:#aaa; text-align:left;">
| | <button id="geo-btn-all" style="background:#333;color:#eee;border:1px solid #555;padding:8px 18px;border-radius:4px;cursor:pointer;">All Time</button> |
| <th style="padding:10px 16px;">#</th>
| | <span id="geo-updated" style="margin-left:auto;color:#555;font-size:0.8em;"></span> |
| <th style="padding:10px 16px;">Country</th>
| |
| <th style="padding:10px 16px; text-align:right;">Users</th>
| |
| <th style="padding:10px 16px;">Share</th>
| |
| <th style="padding:10px 16px; text-align:right;">Edits</th>
| |
| </tr>
| |
| </thead>
| |
| <tbody id="geo-table-body">
| |
| <tr><td colspan="5" style="padding:20px; text-align:center; color:#666;">Loading...</td></tr>
| |
| </tbody>
| |
| </table>
| |
| </div> | | </div> |
|
| |
|
| <div id="geo-activity-chart-wrap" style="background:#1a1a1a; border:1px solid #333; border-radius:6px; padding:16px; margin-top:16px;"> | | <div style="background:#1a1a1a;border:1px solid #333;border-radius:6px;padding:8px;margin-bottom:16px;"> |
| <div style="font-weight:bold; color:#ff6600; margin-bottom:12px; font-size:0.95em;">Daily Activity (Last 30 Days)</div>
| | <svg id="geo-world-map" viewBox="0 0 900 250" style="width:100%;height:auto;display:block;"></svg> |
| <div id="geo-activity-bars" style="display:flex; align-items:flex-end; gap:3px; height:80px; overflow:hidden;"></div>
| |
| <div id="geo-activity-labels" style="display:flex; gap:3px; margin-top:4px; font-size:0.65em; color:#555; overflow:hidden;"></div>
| |
| </div> | | </div> |
|
| |
|
| <div style="margin-top:12px; color:#555; font-size:0.75em;" id="geo-note"> | | <div id="geo-table-wrap" style="background:#1a1a1a;border:1px solid #333;border-radius:6px;overflow:hidden;margin-bottom:16px;"> |
| 📍 Activity data is derived from wiki edit logs. Country detection uses Babel language tags on user pages. Users without Babel tags are shown as "Unknown".
| | <div id="geo-table-title" style="padding:12px 16px;border-bottom:1px solid #333;font-weight:bold;color:#ff6600;font-size:0.95em;">Users by Country</div> |
| | <table style="width:100%;border-collapse:collapse;font-size:0.9em;"> |
| | <thead><tr style="background:#222;color:#aaa;text-align:left;"> |
| | <th style="padding:10px 16px;">#</th> |
| | <th style="padding:10px 16px;">Country / Language</th> |
| | <th style="padding:10px 16px;text-align:right;">Users</th> |
| | <th style="padding:10px 16px;">Share</th> |
| | <th style="padding:10px 16px;text-align:right;">Edits (period)</th> |
| | </tr></thead> |
| | <tbody id="geo-table-body"><tr><td colspan="5" style="padding:20px;text-align:center;color:#666;">Loading...</td></tr></tbody> |
| | </table> |
| </div> | | </div> |
|
| |
|
| | <div style="background:#1a1a1a;border:1px solid #333;border-radius:6px;padding:16px;"> |
| | <div style="font-weight:bold;color:#ff6600;margin-bottom:10px;font-size:0.95em;">Daily Edit Activity — Last 30 Days</div> |
| | <div id="geo-activity-bars" style="display:flex;align-items:flex-end;gap:3px;height:70px;"></div> |
| | <div id="geo-activity-labels" style="display:flex;gap:3px;margin-top:4px;font-size:0.65em;color:#555;"></div> |
| </div> | | </div> |
|
| |
|
| <script> | | <div style="margin-top:12px;color:#555;font-size:0.75em;">📍 Country detection uses Babel language tags on user pages (e.g. <code>{{#babel:de|en}}</code>). Users without Babel tags appear as "Unknown". Edit activity is sourced from the wiki's recent changes log.</div> |
| (function() { | |
| var GEO = window.AlphaXGeo = window.AlphaXGeo || {};
| |
| var API = '/api.php';
| |
|
| |
|
| // ── helpers ──────────────────────────────────────────────────
| | </div> |
| 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());
| |
| } else if (period === 'week') {
| |
| var d = new Date(now); d.setDate(d.getDate() - d.getDay());
| |
| d.setHours(0,0,0,0); return d;
| |
| } else if (period === 'month') {
| |
| return new Date(now.getFullYear(), now.getMonth(), 1);
| |
| }
| |
| return new Date(0);
| |
| }
| |
| | |
| // ── country flags & names ─────────────────────────────────────
| |
| 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'
| |
| };
| |
| | |
| // ── fetch all users ───────────────────────────────────────────
| |
| function fetchUsers() {
| |
| return fetch(API + '?action=query&list=allusers&aulimit=500&format=json&origin=*')
| |
| .then(function(r){return r.json();})
| |
| .then(function(d){ return d.query.allusers || []; });
| |
| }
| |
| | |
| // ── fetch recent changes for activity ────────────────────────
| |
| function fetchRecentChanges(rcstart, rclimit) {
| |
| rclimit = rclimit || 500;
| |
| var url = API + '?action=query&list=recentchanges&rclimit=' + rclimit +
| |
| '&rcprop=user|timestamp&format=json&origin=*';
| |
| if (rcstart) url += '&rcstart=' + rcstart;
| |
| return fetch(url).then(function(r){return r.json();})
| |
| .then(function(d){ return d.query.recentchanges || []; });
| |
| }
| |
| | |
| // ── fetch babel info for a user ───────────────────────────────
| |
| function fetchUserBabel(username) {
| |
| var url = API + '?action=query&titles=User:' + encodeURIComponent(username) +
| |
| '&prop=categories&format=json&origin=*';
| |
| return fetch(url).then(function(r){return r.json();})
| |
| .then(function(d){
| |
| var pages = d.query && d.query.pages ? d.query.pages : {};
| |
| var cats = [];
| |
| Object.values(pages).forEach(function(p){
| |
| (p.categories || []).forEach(function(c){ cats.push(c.title); });
| |
| });
| |
| // Look for Category:User XX patterns
| |
| var langs = [];
| |
| cats.forEach(function(cat){
| |
| var m = cat.match(/Category:User ([a-z]{2,3})/i);
| |
| if (m) langs.push(m[1].toLowerCase());
| |
| });
| |
| return langs;
| |
| });
| |
| }
| |
| | |
| // ── world map SVG (simplified) ────────────────────────────────
| |
| var COUNTRY_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',
| |
| };
| |
| | |
| function drawWorldMap(highlightLangs) {
| |
| var svg = document.getElementById('geo-world-map');
| |
| if (!svg) return;
| |
| // Simple world outline
| |
| svg.innerHTML = '<rect width="900" height="440" fill="#1a1a1a"/>' +
| |
| '<text x="450" y="30" fill="#444" text-anchor="middle" font-size="12" font-family="sans-serif">World Activity Map (language-based approximation)</text>';
| |
| | |
| // Draw ocean
| |
| var ocean = document.createElementNS('http://www.w3.org/2000/svg','rect');
| |
| ocean.setAttribute('width','900'); ocean.setAttribute('height','440');
| |
| ocean.setAttribute('fill','#0d1f33'); svg.appendChild(ocean);
| |
| | |
| // Draw continent blobs
| |
| var continents = [
| |
| // North America
| |
| 'M80,100 L240,95 L255,200 L220,230 L170,235 L110,220 L75,170 Z',
| |
| // South America
| |
| 'M155,240 L220,235 L235,360 L195,385 L155,370 L138,320 Z',
| |
| // Europe
| |
| 'M385,90 L480,85 L490,160 L450,175 L400,170 L378,140 Z',
| |
| // Africa
| |
| 'M390,175 L460,170 L470,320 L430,345 L388,335 L375,280 Z',
| |
| // Asia
| |
| 'M490,80 L750,75 L760,230 L700,250 L520,240 L485,180 Z',
| |
| // Australia
| |
| 'M640,280 L740,275 L748,355 L700,368 L638,355 Z',
| |
| // Greenland
| |
| '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);
| |
| });
| |
| | |
| // Highlight active countries
| |
| if (highlightLangs && highlightLangs.length > 0) {
| |
| Object.keys(COUNTRY_SHAPES).forEach(function(cc){
| |
| var path = document.createElementNS('http://www.w3.org/2000/svg','path');
| |
| path.setAttribute('d', COUNTRY_SHAPES[cc]);
| |
| var isActive = highlightLangs.some(function(l){ return l.startsWith(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);
| |
| });
| |
| }
| |
| | |
| // Dots for known locations
| |
| var locations = [
| |
| {x:160, y:165, label:'North America'},
| |
| {x:450, y:130, label:'Europe'},
| |
| {x:620, y:160, label:'Asia'},
| |
| {x:430, y:260, label:'Africa'},
| |
| {x:190, y:300, label:'South America'},
| |
| {x:690, y:320, label:'Oceania'},
| |
| ];
| |
| locations.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.label;
| |
| svg.appendChild(text);
| |
| });
| |
| }
| |
| | |
| // ── render table ──────────────────────────────────────────────
| |
| 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 === 0) {
| |
| tbody.innerHTML = '<tr><td colspan="5" style="padding:20px;text-align:center;color:#555;">No activity data for this period.</td></tr>';
| |
| return;
| |
| }
| |
| | |
| var html = '';
| |
| rows.forEach(function(row, i) {
| |
| var bar = '<div style="background:#ff6600;height:8px;border-radius:2px;width:' +
| |
| Math.round(row.pct) + '%;min-width:2px;display:inline-block;"></div>' +
| |
| '<span style="color:#666;font-size:0.8em;margin-left:6px;">' + row.pct.toFixed(1) + '%</span>';
| |
| var rowBg = i % 2 === 0 ? '#1a1a1a' : '#1e1e1e';
| |
| html += '<tr style="background:' + rowBg + '; 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;
| |
| }
| |
| | |
| // ── render activity bars ───────────────────────────────────────
| |
| function renderActivityBars(dailyData) {
| |
| var wrap = document.getElementById('geo-activity-bars');
| |
| var labelWrap = document.getElementById('geo-activity-labels');
| |
| if (!wrap) return;
| |
| var max = Math.max.apply(null, Object.values(dailyData).concat([1]));
| |
| var days = [];
| |
| for (var i = 29; i >= 0; i--) {
| |
| var d = new Date(); d.setDate(d.getDate() - i);
| |
| days.push(isoDate(d));
| |
| }
| |
| var barsHtml = '', labelsHtml = '';
| |
| days.forEach(function(day, idx) {
| |
| var val = dailyData[day] || 0;
| |
| var h = Math.max(3, Math.round((val / max) * 70));
| |
| var isToday = idx === 29;
| |
| var color = isToday ? '#ff6600' : (val > 0 ? '#cc5500' : '#222');
| |
| barsHtml += '<div title="' + day + ': ' + val + ' edits" style="flex:1;background:' + color +
| |
| ';height:' + h + 'px;border-radius:2px 2px 0 0;min-width:4px;cursor:default;"></div>';
| |
| if (idx % 5 === 0 || isToday) {
| |
| labelsHtml += '<div style="flex:1;text-align:center;white-space:nowrap;">' + day.slice(5) + '</div>';
| |
| } else {
| |
| labelsHtml += '<div style="flex:1;"></div>';
| |
| }
| |
| });
| |
| wrap.innerHTML = barsHtml;
| |
| labelWrap.innerHTML = labelsHtml;
| |
| }
| |
| | |
| // ── main data load ────────────────────────────────────────────
| |
| var cachedUsers = null;
| |
| var cachedChanges = null;
| |
| var cachedBabel = {};
| |
| var currentPeriod = 'day';
| |
| | |
| function loadDashboard() {
| |
| var now = new Date();
| |
| | |
| Promise.all([
| |
| fetchUsers(),
| |
| fetchRecentChanges(null, 500)
| |
| ]).then(function(results) {
| |
| var users = results[0];
| |
| var changes = results[1];
| |
| cachedUsers = users;
| |
| cachedChanges = changes;
| |
| | |
| document.getElementById('geo-total').textContent = users.length;
| |
| document.getElementById('geo-updated').textContent = 'Updated: ' + now.toLocaleTimeString();
| |
| | |
| // Count activity periods
| |
| var dayStart = startOf('day').toISOString();
| |
| var weekStart = startOf('week').toISOString();
| |
| var monthStart = startOf('month').toISOString();
| |
| | |
| var activeDay = new Set(), activeWeek = new Set(), activeMonth = new Set();
| |
| var daily = {};
| |
| changes.forEach(function(c) {
| |
| if (c.timestamp >= dayStart) activeDay.add(c.user);
| |
| if (c.timestamp >= weekStart) activeWeek.add(c.user);
| |
| if (c.timestamp >= monthStart) activeMonth.add(c.user);
| |
| var day = c.timestamp.slice(0,10);
| |
| daily[day] = (daily[day] || 0) + 1;
| |
| });
| |
| | |
| document.getElementById('geo-active-day').textContent = activeDay.size;
| |
| document.getElementById('geo-active-week').textContent = activeWeek.size;
| |
| document.getElementById('geo-active-month').textContent = activeMonth.size;
| |
| | |
| renderActivityBars(daily);
| |
| | |
| // Fetch babel for all real users (skip bots)
| |
| var humanUsers = users.filter(function(u){
| |
| return !['Lucy','Babel AutoCreate','Delete page script','FuzzyBot','Maintenance script','MediaWiki default'].includes(u.name) || u.name === 'Lucy' || u.name === 'Admin';
| |
| });
| |
| | |
| var babelPromises = humanUsers.map(function(u) {
| |
| return fetchUserBabel(u.name).then(function(langs) {
| |
| cachedBabel[u.name] = langs;
| |
| });
| |
| });
| |
| | |
| Promise.all(babelPromises).then(function() {
| |
| geoShowPeriod(currentPeriod);
| |
| });
| |
| | |
| }).catch(function(e) {
| |
| console.error('GEO dashboard error:', e);
| |
| });
| |
| }
| |
| | |
| window.geoShowPeriod = function(period) {
| |
| currentPeriod = period;
| |
| var btns = ['day','week','month','all'];
| |
| btns.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.border = 'none'; btn.style.fontWeight = 'bold';
| |
| } else {
| |
| btn.style.background = '#333'; btn.style.color = '#eee';
| |
| btn.style.border = '1px solid #555'; btn.style.fontWeight = 'normal';
| |
| }
| |
| });
| |
| | |
| if (!cachedChanges || !cachedUsers) return;
| |
| | |
| var cutoff = period === 'all' ? '1970-01-01' : startOf(period).toISOString();
| |
| | |
| // Build user -> edit count map for period
| |
| var userEdits = {};
| |
| cachedChanges.forEach(function(c) {
| |
| if (c.timestamp >= cutoff) {
| |
| userEdits[c.user] = (userEdits[c.user] || 0) + 1;
| |
| }
| |
| });
| |
| | |
| // Map languages to user counts
| |
| var langUsers = {}, langEdits = {};
| |
| var withBabel = 0;
| |
| | |
| cachedUsers.forEach(function(u) {
| |
| var langs = cachedBabel[u.name] || [];
| |
| if (langs.length === 0) langs = ['unknown'];
| |
| else withBabel++;
| |
| var edits = userEdits[u.name] || 0;
| |
| langs.slice(0,1).forEach(function(lang) {
| |
| langUsers[lang] = (langUsers[lang] || 0) + 1;
| |
| langEdits[lang] = (langEdits[lang] || 0) + edits;
| |
| });
| |
| });
| |
| | |
| var total = cachedUsers.length;
| |
| document.getElementById('geo-countries').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){ return b.users - a.users; });
| |
| | |
| // Move unknown to bottom
| |
| rows.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);
| |
| drawWorldMap(rows.filter(function(r){return r.lang!=='unknown';}).map(function(r){return r.lang;}));
| |
| };
| |
| | |
| // ── boot ─────────────────────────────────────────────────────
| |
| if (document.readyState === 'loading') {
| |
| document.addEventListener('DOMContentLoaded', loadDashboard);
| |
| } else {
| |
| loadDashboard();
| |
| }
| |
| })();
| |
| </script>
| |