AlphaX:Admin Control Panel: Difference between revisions

From AlphaX Wiki
Jump to navigation Jump to search
Create Admin Control Panel with live API-powered article table (words, category, size, revisions, watchers, dates, status)
 
Replaced content with "<!-- Admin Control Panel — content rendered by MediaWiki:Common.js --> __NOTOC__ __NOEDITSECTION__"
Tag: Replaced
 
Line 1: Line 1:
<div id="axcp-root"></div>
<!-- Admin Control Panel — content rendered by MediaWiki:Common.js -->
<style>
__NOTOC__
#axcp-root{font-family:system-ui,-apple-system,sans-serif;background:#0D0D0D;color:#fff;padding:0;margin:0 -10px;}
__NOEDITSECTION__
.axcp-header{background:#1A1A1A;border-bottom:1px solid #2E2E2E;padding:22px 28px 18px;}
.axcp-title{font-size:22px;font-weight:700;color:#fff;margin-bottom:4px;}
.axcp-subtitle{font-size:12px;color:#666;text-transform:uppercase;letter-spacing:.1em;}
.axcp-stats-row{display:flex;gap:12px;margin:16px 0 0;}
.axcp-stat{background:#0D0D0D;border:1px solid #2E2E2E;border-radius:12px;padding:12px 18px;flex:1;}
.axcp-stat-val{font-size:24px;font-weight:700;color:#FF6600;}
.axcp-stat-label{font-size:11px;color:#666;text-transform:uppercase;letter-spacing:.08em;margin-top:2px;}
.axcp-controls{display:flex;gap:10px;padding:16px 28px;background:#0D0D0D;border-bottom:1px solid #2E2E2E;flex-wrap:wrap;align-items:center;}
.axcp-search{flex:1;min-width:200px;background:#1A1A1A;border:1px solid #2E2E2E;border-radius:8px;padding:9px 14px;color:#fff;font-size:14px;outline:none;}
.axcp-search:focus{border-color:rgba(255,102,0,.5);}
.axcp-select{background:#1A1A1A;border:1px solid #2E2E2E;border-radius:8px;padding:9px 14px;color:#fff;font-size:13px;outline:none;cursor:pointer;}
.axcp-select:focus{border-color:rgba(255,102,0,.5);}
.axcp-badge-count{background:rgba(255,102,0,.12);border:1px solid rgba(255,102,0,.3);color:#FF6600;border-radius:6px;padding:3px 10px;font-size:12px;font-weight:600;}
.axcp-table-wrap{overflow-x:auto;padding:0 28px 28px;}
table.axcp-table{width:100%;border-collapse:collapse;margin-top:16px;font-size:13px;}
table.axcp-table th{background:#111;color:#666;text-transform:uppercase;letter-spacing:.08em;font-size:11px;font-weight:600;padding:10px 12px;border-bottom:1px solid #2E2E2E;cursor:pointer;white-space:nowrap;user-select:none;}
table.axcp-table th:hover{color:#FF6600;}
table.axcp-table th.sorted-asc::after{content:" ↑";color:#FF6600;}
table.axcp-table th.sorted-desc::after{content:" ↓";color:#FF6600;}
table.axcp-table td{padding:9px 12px;border-bottom:1px solid rgba(255,255,255,.04);vertical-align:middle;color:#ccc;}
table.axcp-table tr:hover td{background:rgba(255,102,0,.04);}
table.axcp-table tr:last-child td{border-bottom:none;}
.axcp-title-link{color:#fff;text-decoration:none;font-weight:500;}
.axcp-title-link:hover{color:#FF6600;}
.axcp-cat-badge{display:inline-block;padding:2px 8px;border-radius:6px;font-size:11px;font-weight:600;white-space:nowrap;}
.axcp-status{display:inline-block;padding:2px 8px;border-radius:6px;font-size:11px;font-weight:600;}
.status-stub{background:rgba(255,60,60,.15);color:#ff6b6b;border:1px solid rgba(255,60,60,.3);}
.status-short{background:rgba(255,166,0,.15);color:#ffa600;border:1px solid rgba(255,166,0,.3);}
.status-medium{background:rgba(61,220,132,.15);color:#3ddc84;border:1px solid rgba(61,220,132,.3);}
.status-long{background:rgba(100,160,255,.15);color:#64a0ff;border:1px solid rgba(100,160,255,.3);}
.axcp-bar-wrap{background:#1A1A1A;border-radius:4px;height:6px;width:80px;overflow:hidden;display:inline-block;vertical-align:middle;margin-left:6px;}
.axcp-bar{height:6px;border-radius:4px;background:linear-gradient(to right,#FF6600,#FF8533);}
.axcp-loading{text-align:center;padding:60px;color:#666;font-size:15px;}
.axcp-error{text-align:center;padding:40px;color:#ff6b6b;}
.axcp-num{text-align:right;font-variant-numeric:tabular-nums;color:#999;}
.axcp-refresh{background:linear-gradient(135deg,#FF6600,#FF8533);border:none;border-radius:8px;color:#fff;padding:9px 18px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;}
.axcp-refresh:hover{opacity:.85;}
</style>
<script>
(function(){
  var root=document.getElementById('axcp-root');
 
  // Category color map
  var CAT_COLORS={
    'Sexual Health':'rgba(255,100,100,.15)|#ff7a7a',
    'Dating, Sex & Relationships':'rgba(255,182,100,.15)|#FFB664',
    'Kink & BDSM':'rgba(180,100,255,.15)|#c47aff',
    'Culture, History & Politics':'rgba(100,160,255,.15)|#64a0ff',
    'Fashion & Visual Signaling':'rgba(255,220,100,.15)|#ffd664',
    'Community & Identity':'rgba(61,220,132,.15)|#3ddc84',
    'Drugs, Party Culture & Harm Reduction':'rgba(255,120,50,.15)|#ff7832',
    'Life Planning':'rgba(100,200,255,.15)|#64c8ff'
  };
 
  function catStyle(cat){
    var s=CAT_COLORS[cat]||'rgba(150,150,150,.15)|#888';
    var p=s.split('|');
    return 'background:'+p[0]+';color:'+p[1]+';border:1px solid '+p[1].replace(')',', .3)').replace('rgb','rgba')+';';
  }
 
  function wordStatus(w){
    if(w<200)return['stub','Stub'];
    if(w<600)return['short','Short'];
    if(w<1500)return['medium','Medium'];
    return['long','Long'];
  }
 
  function fmtDate(iso){
    if(!iso)return'—';
    var d=new Date(iso);
    return d.toLocaleDateString('en-GB',{day:'2-digit',month:'short',year:'numeric'});
  }
 
  function fmtSize(bytes){
    if(!bytes)return'—';
    return(bytes/1024).toFixed(1)+' KB';
  }
 
  function escHtml(s){
    return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  }
 
  var allData=[];
  var sortCol='title';
  var sortDir=1;
  var filterText='';
  var filterCat='';
 
  function render(data){
    var filtered=data.filter(function(r){
      var matchText=!filterText||r.title.toLowerCase().indexOf(filterText)>=0||(r.cat||'').toLowerCase().indexOf(filterText)>=0;
      var matchCat=!filterCat||r.cat===filterCat;
      return matchText&&matchCat;
    });
 
    filtered.sort(function(a,b){
      var av=a[sortCol]||'',bv=b[sortCol]||'';
      if(typeof av==='number'&&typeof bv==='number')return sortDir*(av-bv);
      return sortDir*String(av).localeCompare(String(bv));
    });
 
    var maxWords=Math.max.apply(null,filtered.map(function(r){return r.words||0;}));
 
    // Stats
    var totalWords=filtered.reduce(function(s,r){return s+(r.words||0);},0);
    var avgWords=filtered.length?Math.round(totalWords/filtered.length):0;
    var totalRevs=filtered.reduce(function(s,r){return s+(r.revisions||0);},0);
 
    // Build cats for dropdown
    var cats={};
    data.forEach(function(r){if(r.cat)cats[r.cat]=1;});
    var catList=Object.keys(cats).sort();
 
    var html='';
    // Header
    html+='<div class="axcp-header">';
    html+='<div class="axcp-title">&#9881; Admin Control Panel</div>';
    html+='<div class="axcp-subtitle">AlphaX Wiki &mdash; Article Database &mdash; Live Data</div>';
    html+='<div class="axcp-stats-row">';
    html+='<div class="axcp-stat"><div class="axcp-stat-val">'+filtered.length+'</div><div class="axcp-stat-label">Articles</div></div>';
    html+='<div class="axcp-stat"><div class="axcp-stat-val">'+avgWords.toLocaleString()+'</div><div class="axcp-stat-label">Avg Words</div></div>';
    html+='<div class="axcp-stat"><div class="axcp-stat-val">'+totalWords.toLocaleString()+'</div><div class="axcp-stat-label">Total Words</div></div>';
    html+='<div class="axcp-stat"><div class="axcp-stat-val">'+totalRevs.toLocaleString()+'</div><div class="axcp-stat-label">Total Revisions</div></div>';
    html+='<div class="axcp-stat"><div class="axcp-stat-val">'+catList.length+'</div><div class="axcp-stat-label">Categories</div></div>';
    html+='</div></div>';
 
    // Controls
    html+='<div class="axcp-controls">';
    html+='<input class="axcp-search" id="axcp-search" type="text" placeholder="&#128269; Search articles..." value="'+escHtml(filterText)+'">';
    html+='<select class="axcp-select" id="axcp-cat-filter"><option value="">All Categories</option>';
    catList.forEach(function(c){html+='<option value="'+escHtml(c)+'"'+(filterCat===c?' selected':'')+'>'+escHtml(c)+'</option>';});
    html+='</select>';
    html+='<span class="axcp-badge-count">'+filtered.length+' articles</span>';
    html+='<button class="axcp-refresh" id="axcp-reload">&#8635; Refresh Data</button>';
    html+='</div>';
 
    // Table
    html+='<div class="axcp-table-wrap"><table class="axcp-table">';
    html+='<thead><tr>';
    var cols=[
      {key:'title',label:'Article'},
      {key:'cat',label:'Category'},
      {key:'subcat',label:'Subcategory'},
      {key:'words',label:'Words'},
      {key:'size',label:'Size'},
      {key:'status',label:'Status'},
      {key:'created',label:'Created'},
      {key:'touched',label:'Last Edited'},
      {key:'revisions',label:'Revisions'},
      {key:'watchers',label:'Watchers'},
      {key:'links',label:'Inbound Links'}
    ];
    cols.forEach(function(c){
      var cls=sortCol===c.key?(sortDir===1?'sorted-asc':'sorted-desc'):'';
      html+='<th class="'+cls+'" data-col="'+c.key+'">'+c.label+'</th>';
    });
    html+='</tr></thead><tbody>';
 
    if(filtered.length===0){
      html+='<tr><td colspan="11" style="text-align:center;padding:40px;color:#666;">No articles match your filters.</td></tr>';
    }
    filtered.forEach(function(r){
      var st=wordStatus(r.words||0);
      var barW=maxWords>0?Math.round(((r.words||0)/maxWords)*80):0;
      var cs=catStyle(r.cat||'');
      html+='<tr>';
      html+='<td><a class="axcp-title-link" href="/wiki/'+encodeURIComponent((r.title||'').replace(/ /g,'_'))+'" target="_blank">'+escHtml(r.title||'')+'</a></td>';
      html+='<td>'+(r.cat?'<span class="axcp-cat-badge" style="'+cs+'">'+escHtml(r.cat)+'</span>':'<span style="color:#444">—</span>')+'</td>';
      html+='<td style="color:#777;font-size:12px;">'+escHtml(r.subcat||'—')+'</td>';
      html+='<td class="axcp-num">'+((r.words||0).toLocaleString())+'<span class="axcp-bar-wrap"><span class="axcp-bar" style="width:'+barW+'px"></span></span></td>';
      html+='<td class="axcp-num">'+fmtSize(r.size)+'</td>';
      html+='<td><span class="axcp-status status-'+st[0]+'">'+st[1]+'</span></td>';
      html+='<td style="color:#777;white-space:nowrap;">'+fmtDate(r.created)+'</td>';
      html+='<td style="color:#777;white-space:nowrap;">'+fmtDate(r.touched)+'</td>';
      html+='<td class="axcp-num" style="color:'+(r.revisions>10?'#FF6600':'#777')+'">'+((r.revisions||0))+'</td>';
      html+='<td class="axcp-num" style="color:'+(r.watchers>0?'#3ddc84':'#444')+'">'+(r.watchers!=null?(r.watchers>0?'&#128065; '+r.watchers:'0'):'')+'</td>';
      html+='<td class="axcp-num">'+((r.links||0))+'</td>';
      html+='</tr>';
    });
    html+='</tbody></table></div>';
 
    root.innerHTML=html;
 
    // Bind events
    document.getElementById('axcp-search').addEventListener('input',function(){filterText=this.value.toLowerCase();render(allData);});
    document.getElementById('axcp-cat-filter').addEventListener('change',function(){filterCat=this.value;render(allData);});
    document.getElementById('axcp-reload').addEventListener('click',function(){loadData();});
    document.querySelectorAll('table.axcp-table 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);
      });
    });
  }
 
  async function loadData(){
    root.innerHTML='<div class="axcp-loading">&#9881; Loading article data from API&hellip;</div>';
 
    try{
      // Step 1: get all article page IDs + basic info
      var pages=[];
      var apcontinue=null;
      do{
        var url='/api.php?action=query&list=allpages&apnamespace=0&aplimit=500&format=json';
        if(apcontinue)url+='&apcontinue='+encodeURIComponent(apcontinue);
        var r=await fetch(url);
        var d=await r.json();
        pages=pages.concat(d.query.allpages);
        apcontinue=d.continue?d.continue.apcontinue:null;
      }while(apcontinue);
 
      // Step 2: batch fetch page props (size, touched, lastrevid, categories) in chunks of 50
      var map={};
      pages.forEach(function(p){map[p.pageid]={title:p.title,cat:'',subcat:'',words:0,size:0,created:'',touched:'',revisions:0,watchers:null,links:0};});
 
      var ids=pages.map(function(p){return p.pageid;});
      for(var i=0;i<ids.length;i+=50){
        var chunk=ids.slice(i,i+50).join('|');
        var r2=await fetch('/api.php?action=query&pageids='+chunk+'&prop=info|categories|revisions&inprop=watchers&rvprop=size|timestamp&rvdir=newer&rvlimit=1&cllimit=10&format=json');
        var d2=await r2.json();
        Object.keys(d2.query.pages).forEach(function(pid){
          var pg=d2.query.pages[pid];
          if(!map[pid])return;
          map[pid].size=pg.length||0;
          map[pid].touched=pg.touched||'';
          map[pid].watchers=pg.watchers!=null?pg.watchers:null;
          // categories
          if(pg.categories){
            pg.categories.forEach(function(c){
              var cn=c.title.replace('Category:','');
              var knownCats=['Sexual Health','Dating, Sex & Relationships','Kink & BDSM','Culture, History & Politics','Fashion & Visual Signaling','Community & Identity','Drugs, Party Culture & Harm Reduction','Life Planning'];
              if(knownCats.indexOf(cn)>=0){
                if(!map[pid].cat)map[pid].cat=cn;
              }else if(!cn.startsWith('Pages') && !cn.startsWith('Articles')){
                if(!map[pid].subcat)map[pid].subcat=cn;
              }
            });
          }
          // creation date from first revision
          if(pg.revisions&&pg.revisions[0])map[pid].created=pg.revisions[0].timestamp||'';
        });
      }
 
      // Step 3: get revision counts and word counts via separate calls
      for(var j=0;j<ids.length;j+=50){
        var chunk2=ids.slice(j,j+50).join('|');
        // revision count via revisions prop count
        var r3=await fetch('/api.php?action=query&pageids='+chunk2+'&prop=revisions&rvprop=ids&rvlimit=max&format=json');
        var d3=await r3.json();
        Object.keys(d3.query.pages).forEach(function(pid){
          var pg=d3.query.pages[pid];
          if(!map[pid])return;
          map[pid].revisions=(pg.revisions||[]).length;
        });
      }
 
      // Step 4: get wikitext word counts (batch extracts)
      for(var k=0;k<ids.length;k+=20){
        var chunk3=ids.slice(k,k+20).join('|');
        var r4=await fetch('/api.php?action=query&pageids='+chunk3+'&prop=revisions&rvprop=content&rvslots=main&format=json');
        var d4=await r4.json();
        Object.keys(d4.query.pages).forEach(function(pid){
          var pg=d4.query.pages[pid];
          if(!map[pid]||!pg.revisions||!pg.revisions[0])return;
          var content=pg.revisions[0].slots?pg.revisions[0].slots.main['*']:(pg.revisions[0]['*']||'');
          // Strip wiki markup
          var clean=content
            .replace(/<!--[sS]*?-->/g,'')
            .replace(/<[^>]+>/g,' ')
            .replace(/[[File:[^]]+]]/gi,'')
            .replace(/[[([^]|]+|)?([^]]+)]]/g,'$2')
            .replace(/{{[^}]+}}/g,'')
            .replace(/={2,}[^=]+=={2,}/g,' ')
            .replace(/[*#:;|!]/g,' ')
            .replace(/https?://S+/g,'')
            .replace(/s+/g,' ').trim();
          map[pid].words=clean?clean.split(/s+/).filter(function(w){return w.length>0;}).length:0;
        });
      }
 
      // Step 5: inbound links count
      for(var l=0;l<ids.length;l+=50){
        var chunk4=ids.slice(l,l+50).join('|');
        var r5=await fetch('/api.php?action=query&pageids='+chunk4+'&prop=linkshere&lhnamespace=0&lhlimit=max&format=json');
        var d5=await r5.json();
        Object.keys(d5.query.pages).forEach(function(pid){
          var pg=d5.query.pages[pid];
          if(!map[pid])return;
          map[pid].links=(pg.linkshere||[]).length;
        });
      }
 
      allData=Object.values(map);
      render(allData);
 
    }catch(e){
      root.innerHTML='<div class="axcp-error">&#9888; Error loading data: '+e.message+'</div>';
    }
  }
 
  loadData();
})();
</script>

Latest revision as of 01:35, 21 April 2026