|
|
| 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,'&').replace(/</g,'<').replace(/>/g,'>');
| |
| }
| |
| | |
| 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">⚙ Admin Control Panel</div>';
| |
| html+='<div class="axcp-subtitle">AlphaX Wiki — Article Database — 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="🔍 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">↻ 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?'👁 '+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">⚙ Loading article data from API…</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">⚠ Error loading data: '+e.message+'</div>';
| |
| }
| |
| }
| |
| | |
| loadData();
| |
| })();
| |
| </script>
| |