User:Gauri Guptaa/pageHealthDashboard.js
Appearance
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
| Documentation for this user script can be added at User:Gauri Guptaa/pageHealthDashboard. |
/**
* Displays an inline page health dashboard on Wikipedia (and other
* Wikimedia wikis) article pages, showing colour-graded metrics that
* would otherwise require navigating to the separate action=info page
* or running external tools.
*
* Redundancy check:
* MediaWiki's built-in action=info page shows page size, edit count,
* watchers, and a link to pageview statistics β but only on a *separate*
* page, never inline on the article itself. No native feature shows
* per-section citation density, maintenance-tag count, or a composite
* health score inline while reading. This script fills that gap.
*
* Metrics shown (all fetched from the same-domain MediaWiki API):
* π Article size β bytes, colour-graded green/amber/red
* π Word count β estimated from prose text nodes
* π Wikilinks β internal link count
* π References β total <ref> citations on the page
* β οΈ Maintenance tags β count of {{stub}}, {{cleanup}}, {{unreferenced}}
* and similar maintenance templates (prop=templates,
* filtered by Template:Ambox members)
* π Sections β number of headings
* ποΈ Pageviews (30d) β via Wikimedia Pageviews REST API
* (wikimedia.org β allowed by Wikimedia CSP)
* π Last edited β human-readable relative timestamp
*
* A composite "health score" (0β100) is derived from the above and shown
* as a colour-coded badge: green (good), amber (needs work), red (poor).
*
* The dashboard is injected as a collapsible panel at the top of the
* article content area. It can be dismissed and will not reappear until
* the page is reloaded.
*
* APIs used:
* mw.Api β action=query, prop=info|revisions|templates|categories
* Wikimedia REST API β /api/rest_v1/metrics/pageviews (same *.wikimedia.org
* origin β permitted by Wikimedia CSP)
* mw.util.addCSS, mw.config, mw.hook('wikipage.content')
*
* <nowiki>
* Where to save:
* commons.wikimedia.org/wiki/User:Gauri_Guptaa/pageHealthDashboard.js
*
* Add to each wiki's common.js:
* mw.loader.load('//commons.wikimedia.org/w/index.php?title=User:Gauri_Guptaa/pageHealthDashboard.js&action=raw&ctype=text/javascript');
*
* Works on: Wikipedia (all languages), Commons, Wikidata β NS 0 only.
* </nowiki>
*
* @author [[User:Gauri_Guptaa]]
* @source [[User:Gauri_Guptaa/pageHealthDashboard.js]]
* @license CC0-1.0
*/
/* global mw */
( function () {
'use strict';
if ( mw.config.get( 'wgNamespaceNumber' ) !== 0 ) {
return;
}
if ( mw.config.get( 'wgAction' ) !== 'view' ) {
return;
}
var PANEL_ID = 'phd-panel';
// ββ Maintenance template names that signal quality issues βββββββββββββββββ
// These are Template: page names (without "Template:" prefix).
// Filtered from prop=templates response.
var MAINT_RE = /^(stub|cleanup|unreferenced|refimprove|expand|wikify|orphan|advert|coi|pov|original.research|notability|merge|split|update|outdated|dead.end|citation.needed|medref|primarysources|improve)/i;
// ββ Styles ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
mw.util.addCSS(
'#' + PANEL_ID + '{' +
'border:1px solid #a2a9b1;border-radius:6px;' +
'margin-bottom:1em;font-size:13px;clear:both;' +
'background:var(--background-color-base,#fff);' +
'overflow:hidden;' +
'}' +
'#phd-head{' +
'display:flex;align-items:center;justify-content:space-between;' +
'padding:8px 14px;cursor:pointer;user-select:none;' +
'background:var(--background-color-interactive,#f8f9fa);' +
'border-bottom:1px solid #a2a9b1;font-weight:700;' +
'}' +
'#phd-head.phd-collapsed{border-bottom:0}' +
'#phd-toggle{font-size:11px;font-weight:400;color:#54595d}' +
'#phd-body{padding:12px 14px}' +
'#phd-body.phd-hidden{display:none}' +
'#phd-score-row{' +
'display:flex;align-items:center;gap:14px;margin-bottom:12px;' +
'padding-bottom:10px;border-bottom:1px solid #eaecf0;' +
'}' +
'#phd-score-badge{' +
'width:52px;height:52px;border-radius:50%;' +
'display:flex;align-items:center;justify-content:center;' +
'font-size:18px;font-weight:700;color:#fff;flex-shrink:0;' +
'}' +
'#phd-score-label{font-size:12px;color:#54595d;margin-top:2px}' +
'#phd-metrics{' +
'display:grid;' +
'grid-template-columns:repeat(auto-fill,minmax(130px,1fr));' +
'gap:8px;' +
'}' +
'.phd-metric{' +
'background:var(--background-color-interactive,#f8f9fa);' +
'border:1px solid #eaecf0;border-radius:6px;' +
'padding:7px 10px;' +
'}' +
'.phd-metric-val{' +
'font-size:16px;font-weight:700;color:#202122;line-height:1;' +
'}' +
'.phd-metric-lbl{font-size:11px;color:#54595d;margin-top:3px}' +
'.phd-metric-bar{' +
'height:3px;border-radius:2px;margin-top:5px;' +
'background:#eaecf0;overflow:hidden;' +
'}' +
'.phd-metric-fill{height:100%;border-radius:2px;transition:width .4s}' +
'.phd-green{color:#14803c}.phd-amber{color:#a66800}.phd-red{color:#c00}' +
'.phd-fill-green{background:#14803c}' +
'.phd-fill-amber{background:#a66800}' +
'.phd-fill-red{background:#c00}' +
'#phd-footer{' +
'margin-top:10px;padding-top:8px;border-top:1px solid #eaecf0;' +
'font-size:11px;color:#72777d;display:flex;' +
'justify-content:space-between;align-items:center;' +
'}' +
'#phd-footer a{color:#3366cc;font-size:11px}' +
'#phd-close-btn{' +
'cursor:pointer;color:#72777d;background:none;border:0;' +
'padding:0;font-size:15px;line-height:1;margin-left:8px;' +
'}'
);
// ββ Grading helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/**
* Return a CSS class suffix based on a 0β1 ratio and thresholds.
* ratio β₯ goodThreshold β green, β₯ warnThreshold β amber, else red.
*
* @param {number} ratio
* @param {number} goodThreshold
* @param {number} warnThreshold
* @return {string} 'green' | 'amber' | 'red'
*/
function grade( ratio, goodThreshold, warnThreshold ) {
if ( ratio >= goodThreshold ) {
return 'green';
}
if ( ratio >= warnThreshold ) {
return 'amber';
}
return 'red';
}
function relTime( iso ) {
var diff = Date.now() - new Date( iso ).getTime();
var mins = Math.floor( diff / 60000 );
if ( mins < 60 ) {
return mins + ' min ago';
}
var hrs = Math.floor( mins / 60 );
if ( hrs < 24 ) {
return hrs + ' hr ago';
}
var days = Math.floor( hrs / 24 );
if ( days < 30 ) {
return days + ' day' + ( days !== 1 ? 's' : '' ) + ' ago';
}
var mos = Math.floor( days / 30 );
return mos + ' month' + ( mos !== 1 ? 's' : '' ) + ' ago';
}
// ββ DOM metric helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββ
/** Count words in the prose text of the article. */
function countWords( parser ) {
var text = '';
parser.querySelectorAll( 'p' ).forEach( function ( p ) {
if ( !p.closest( '.infobox,.navbox,.reflist,table' ) ) {
text += ' ' + p.textContent;
}
} );
return text.trim().split( /\s+/ ).filter( Boolean ).length;
}
/** Count inline <ref> citations rendered as footnote links. */
function countRefs( parser ) {
return parser.querySelectorAll(
'sup.reference, .mw-ref, ol.references > li'
).length;
}
/** Count internal wikilinks. */
function countLinks( parser ) {
return parser.querySelectorAll(
'#mw-content-text a[href^="/wiki/"]:not(.image):not(.new)'
).length;
}
/** Count section headings (h2 + h3). */
function countSections( parser ) {
return parser.querySelectorAll( 'h2,h3' ).length;
}
// ββ Composite health score ββββββββββββββββββββββββββββββββββββββββββββββββ
/**
* Compute a 0β100 health score from the gathered metrics.
* Weights are intentionally simple and readable.
*
* @param {Object} m metrics object
* @return {number}
*/
function healthScore( m ) {
var score = 0;
// References (0β30 points): >20 refs = full marks
score += Math.min( 30, ( m.refs / 20 ) * 30 );
// Article size (0β20 points): >20 kB = full marks
score += Math.min( 20, ( m.bytes / 20000 ) * 20 );
// Word count (0β20 points): >2000 words = full marks
score += Math.min( 20, ( m.words / 2000 ) * 20 );
// Wikilinks (0β15 points): >30 links = full marks
score += Math.min( 15, ( m.links / 30 ) * 15 );
// Maintenance tags penalty (0 to -15 points)
score -= Math.min( 15, m.maintTags * 5 );
return Math.max( 0, Math.min( 100, Math.round( score ) ) );
}
function scoreColor( s ) {
if ( s >= 70 ) {
return '#14803c';
}
if ( s >= 40 ) {
return '#a66800';
}
return '#c00';
}
function scoreLabel( s ) {
if ( s >= 70 ) {
return 'Good';
}
if ( s >= 40 ) {
return 'Needs work';
}
return 'Poor';
}
// ββ Build metric card βββββββββββββββββββββββββββββββββββββββββββββββββββββ
function metricCard( icon, label, value, fillPct, gradeStr ) {
var $card = $( '<div class="phd-metric">' );
var $val = $( '<div class="phd-metric-val">' )
.text( icon + ' ' + value );
if ( gradeStr ) {
$val.addClass( 'phd-' + gradeStr );
}
$card.append(
$val,
$( '<div class="phd-metric-lbl">' ).text( label )
);
if ( fillPct !== null ) {
$card.append(
$( '<div class="phd-metric-bar">' ).append(
$( '<div class="phd-metric-fill phd-fill-' + ( gradeStr || 'green' ) + '">' )
.css( 'width', Math.min( 100, fillPct ) + '%' )
)
);
}
return $card;
}
// ββ Render dashboard ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function render( parser, m ) {
var score = healthScore( m );
var sColor = scoreColor( score );
var sLabel = scoreLabel( score );
var $panel = $( '#' + PANEL_ID );
var $body = $panel.find( '#phd-body' );
$body.empty();
// Score row
var $scoreRow = $( '<div id="phd-score-row">' );
$( '<div id="phd-score-badge">' )
.css( 'background', sColor )
.text( score )
.appendTo( $scoreRow );
$( '<div>' ).append(
$( '<div>' ).css( { fontWeight: '700', fontSize: '14px' } )
.text( sLabel ),
$( '<div id="phd-score-label">' )
.text( 'Page health score Β· based on size, refs, links, tags' )
).appendTo( $scoreRow );
$body.append( $scoreRow );
// Metrics grid
var $grid = $( '<div id="phd-metrics">' );
// Size
var kb = ( m.bytes / 1024 ).toFixed( 1 );
var sizeGrade = grade( m.bytes / 30000, 0.67, 0.33 );
$grid.append( metricCard( 'π', 'Article size',
kb + ' kB', m.bytes / 300, sizeGrade ) );
// Words
var wGrade = grade( m.words / 2000, 1, 0.5 );
$grid.append( metricCard( 'π', 'Word count',
m.words.toLocaleString(), m.words / 20, wGrade ) );
// References
var rGrade = grade( m.refs / 20, 1, 0.5 );
$grid.append( metricCard( 'π', 'References',
m.refs, m.refs * 5, rGrade ) );
// Wikilinks
var lGrade = grade( m.links / 30, 1, 0.5 );
$grid.append( metricCard( 'π', 'Wikilinks',
m.links, m.links * 3, lGrade ) );
// Sections
$grid.append( metricCard( 'π', 'Sections', m.sections, null, null ) );
// Maintenance tags
var tGrade = m.maintTags === 0 ? 'green' :
m.maintTags <= 2 ? 'amber' : 'red';
$grid.append( metricCard( 'β οΈ', 'Maint. tags',
m.maintTags === 0 ? 'None' : m.maintTags,
null, tGrade ) );
// Pageviews
if ( m.pageviews !== null ) {
$grid.append( metricCard( 'ποΈ', 'Views (30d)',
m.pageviews.toLocaleString(), null, null ) );
}
// Last edited
if ( m.lastEdit ) {
$grid.append( metricCard( 'π', 'Last edited',
relTime( m.lastEdit ), null, null ) );
}
$body.append( $grid );
// Footer with link to full action=info page
var infoUrl = mw.util.getUrl( mw.config.get( 'wgPageName' ),
{ action: 'info' } );
$body.append(
$( '<div id="phd-footer">' ).append(
$( '<span>' ).text( 'Metrics from MediaWiki API Β· DOM analysis' ),
$( '<a>' ).attr( { href: infoUrl } ).text( 'Full page info β' )
)
);
}
// ββ Fetch pageviews from Wikimedia REST API βββββββββββββββββββββββββββββββ
// wikimedia.org is in Wikimedia's own CSP allowlist.
function fetchPageviews( title, project ) {
var end = new Date();
var start = new Date( end );
start.setDate( start.getDate() - 30 );
function pad( n ) {
return String( n ).padStart( 2, '0' );
}
function fmt( d ) {
return d.getFullYear() +
pad( d.getMonth() + 1 ) +
pad( d.getDate() ) + '00';
}
var url = 'https://wikimedia.org/api/rest_v1/metrics/pageviews/per-article/' +
encodeURIComponent( project ) + '/all-access/user/' +
encodeURIComponent( title.replace( / /g, '_' ) ) +
'/daily/' + fmt( start ) + '/' + fmt( end );
var d = $.Deferred();
$.ajax( {
url: url,
dataType: 'json',
timeout: 8000
} ).done( function ( data ) {
var total = ( data.items || [] ).reduce( function ( s, i ) {
return s + ( i.views || 0 );
}, 0 );
d.resolve( total );
} ).fail( function () {
d.resolve( null );
} );
return d.promise();
}
// ββ Main process ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function process( $content ) {
var parser = $content[ 0 ];
if ( parser.dataset.phdDone ) {
return;
}
parser.dataset.phdDone = '1';
var title = mw.config.get( 'wgPageName' );
var host = window.location.hostname; // e.g. en.wikipedia.org
// Wikimedia pageviews project string: en.wikipedia, fr.wikipedia, etc.
var project = host.replace( /^www\./, '' );
// Build panel skeleton immediately.
var $panel = $( '<div id="' + PANEL_ID + '">' );
var $head = $( '<div id="phd-head">' ).append(
$( '<span>' ).html( 'π Page health dashboard' ),
$( '<div>' ).append(
$( '<span id="phd-toggle">' ).text( 'β² collapse' ),
$( '<button id="phd-close-btn" type="button">' )
.text( 'β' ).attr( 'title', 'Dismiss' )
.on( 'click', function ( e ) {
e.stopPropagation();
$panel.remove();
} )
)
).on( 'click', function () {
var c = $head.hasClass( 'phd-collapsed' );
$head.toggleClass( 'phd-collapsed', !c );
$body.toggleClass( 'phd-hidden', !c );
$( '#phd-toggle' ).text( c ? 'β² collapse' : 'βΌ expand' );
} );
var $body = $( '<div id="phd-body">' )
.html( '<span style="color:#72777d;font-style:italic">Loading metricsβ¦</span>' );
$panel.append( $head, $body );
$( parser ).prepend( $panel );
// Collect DOM metrics (synchronous β no API needed).
var domMetrics = {
words: countWords( parser ),
refs: countRefs( parser ),
links: countLinks( parser ),
sections: countSections( parser )
};
// API call: page size, last-edit timestamp, maintenance templates.
var apiCall = new mw.Api().get( {
action: 'query',
titles: title,
prop: 'info|revisions|templates',
inprop: 'url',
rvprop: 'timestamp',
rvlimit: 1,
tllimit: 'max',
tlnamespace: 10, // NS_TEMPLATE
format: 'json'
} );
$.when( apiCall, fetchPageviews( title, project ) )
.done( function ( apiData, pv ) {
var pages = apiData[ 0 ].query.pages;
var page = pages[ Object.keys( pages )[ 0 ] ];
var bytes = page.length || 0;
var lastEdit = ( page.revisions && page.revisions[ 0 ] ) ?
page.revisions[ 0 ].timestamp : null;
// Filter maintenance templates.
var maintTags = ( page.templates || [] ).filter( function ( t ) {
var name = t.title.replace( /^Template:/, '' );
return MAINT_RE.test( name );
} ).length;
render( parser, {
bytes: bytes,
words: domMetrics.words,
refs: domMetrics.refs,
links: domMetrics.links,
sections: domMetrics.sections,
maintTags: maintTags,
pageviews: pv,
lastEdit: lastEdit
} );
} )
.fail( function () {
$body.html( '<span style="color:#c00">Could not load metrics.</span>' );
} );
}
mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] ).then( function () {
mw.hook( 'wikipage.content' ).add( process );
} );
}() );