MediaWiki:Gadget-ConfirmationStore.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* DiagnosticTree — Confirmation Store v2
*
* Changes from v1:
* - Anonymous confirmation disabled entirely
* - Actor is always a username (physician group required)
* - MAX_SESSIONS_PER_MUSCLE increased to 50
* - isPhysician() helper exposed for UI gating
*
* Storage: Data:Confirmations/<TreeId>
* Format: { muscle_id: { n, last, sessions: [{ts, actor}] } }
*/
( function ( mw, $ ) {
'use strict';
const MAX_SESSIONS_PER_MUSCLE = 50;
const DATA_PAGE_PREFIX = 'Data:Confirmations/';
const LS_KEY_PREFIX = 'painwiki_confirmations_';
const REQUIRED_GROUP = 'physician';
// ── Access control ────────────────────────────────────────────────
/**
* Returns true if the current user is in the 'physician' group.
* Used to gate the confirm button in the UI.
*/
function isPhysician() {
const groups = mw.config.get( 'wgUserGroups' ) || [];
return groups.indexOf( REQUIRED_GROUP ) !== -1;
}
/**
* Returns true if the user is logged in at all.
*/
function isLoggedIn() {
return !!mw.config.get( 'wgUserName' );
}
/**
* Returns the current username, or null if not logged in.
*/
function getActor() {
return mw.config.get( 'wgUserName' ) || null;
}
// ── Timestamp ─────────────────────────────────────────────────────
function nowISO() {
return new Date().toISOString();
}
// ── Read ──────────────────────────────────────────────────────────
function loadConfirmations( treeId ) {
const api = new mw.Api();
const lsKey = LS_KEY_PREFIX + treeId;
return api.get( {
action: 'query',
prop: 'revisions',
titles: DATA_PAGE_PREFIX + treeId,
rvprop: 'content',
rvslots: 'main',
formatversion: 2
} ).then( function ( data ) {
const pages = data.query && data.query.pages;
if ( !pages || !pages.length ) return localFallback( lsKey );
const page = pages[0];
if ( page.missing ) return localFallback( lsKey );
const content = page.revisions &&
page.revisions[0] &&
page.revisions[0].slots &&
page.revisions[0].slots.main &&
page.revisions[0].slots.main.content;
if ( !content ) return localFallback( lsKey );
try {
const parsed = JSON.parse( content );
localStorage.setItem( lsKey, JSON.stringify( parsed ) );
return parsed;
} catch ( e ) {
return localFallback( lsKey );
}
} ).catch( function () {
return localFallback( lsKey );
} );
}
function localFallback( lsKey ) {
try {
const raw = localStorage.getItem( lsKey );
if ( raw ) return JSON.parse( raw );
} catch ( e ) {}
return {};
}
// ── Write ─────────────────────────────────────────────────────────
/**
* Records a confirmation.
* Rejects immediately if the user is not in the physician group.
*
* @returns Promise resolving to updated counts, or rejecting with
* 'not-physician' | 'not-logged-in' error strings.
*/
function confirmMuscle( treeId, muscleId, currentCounts ) {
if ( !isLoggedIn() ) {
return Promise.reject( 'not-logged-in' );
}
if ( !isPhysician() ) {
return Promise.reject( 'not-physician' );
}
const actor = getActor();
const counts = JSON.parse( JSON.stringify( currentCounts ) );
if ( !counts[muscleId] ) {
counts[muscleId] = { n: 0, last: null, sessions: [] };
}
const entry = counts[muscleId];
const ts = nowISO();
entry.n += 1;
entry.last = ts;
entry.sessions.push( { ts: ts, actor: actor } );
// Enforce rolling window
if ( entry.sessions.length > MAX_SESSIONS_PER_MUSCLE ) {
entry.sessions = entry.sessions.slice(
entry.sessions.length - MAX_SESSIONS_PER_MUSCLE
);
}
return writeConfirmations( treeId, counts ).then( function () {
// Also update local cache
localStorage.setItem(
LS_KEY_PREFIX + treeId,
JSON.stringify( counts )
);
return counts;
} ).catch( function ( err ) {
// Write failed (network, permissions) — update local cache only
localStorage.setItem(
LS_KEY_PREFIX + treeId,
JSON.stringify( counts )
);
mw.log.warn( 'PainWiki: confirmation write failed —', err );
return counts;
} );
}
function writeConfirmations( treeId, counts ) {
const api = new mw.Api();
return api.postWithToken( 'csrf', {
action: 'edit',
title: DATA_PAGE_PREFIX + treeId,
text: JSON.stringify( counts, null, 2 ),
summary: 'Confirmation: ' + mw.config.get( 'wgUserName' ),
minor: true,
contentmodel: 'json',
formatversion: 2
} );
}
// ── Priors ────────────────────────────────────────────────────────
function computeUpdatedPriors( schemaMuscles, counts, learningRate ) {
learningRate = learningRate || 0.02;
const ids = Object.keys( schemaMuscles );
const p = {};
ids.forEach( function ( mid ) {
const base = schemaMuscles[mid].prior || 0.1;
const n = ( counts[mid] && counts[mid].n ) || 0;
p[mid] = base + n * learningRate;
} );
let total = 0;
ids.forEach( function ( mid ) { total += p[mid]; } );
ids.forEach( function ( mid ) { p[mid] /= total; } );
return p;
}
// ── Public API ────────────────────────────────────────────────────
window.PainWikiConfirmations = {
load: loadConfirmations,
confirm: confirmMuscle,
computePriors: computeUpdatedPriors,
isPhysician: isPhysician,
isLoggedIn: isLoggedIn,
MAX_SESSIONS: MAX_SESSIONS_PER_MUSCLE
};
} )( mediaWiki, jQuery );
/*
* ── UI integration notes ───────────────────────────────────────────
*
* In DiagnosticTree.js, gate the confirm button on render:
*
* const canConfirm = PainWikiConfirmations.isPhysician();
*
* // If physician: render normal confirm button
* // If logged in but not physician: render greyed button with tooltip
* // "Physician verification required — contact site admin"
* // If not logged in: render login prompt link instead of button
*
* Example:
*
* function confirmButtonHTML( muscleId, label ) {
* if ( !PainWikiConfirmations.isLoggedIn() ) {
* return '<a href="/wiki/Special:UserLogin" class="dt-confirm-btn dt-confirm-login">' +
* 'Log in to confirm</a>';
* }
* if ( !PainWikiConfirmations.isPhysician() ) {
* return '<button class="dt-confirm-btn dt-confirm-btn--disabled" disabled ' +
* 'title="Physician verification required">' +
* '🔒 Physician access required</button>';
* }
* return '<button class="dt-confirm-btn" data-muscle="' + muscleId + '" ' +
* 'data-label="' + encodeURIComponent(label) + '">✓ Confirm</button>';
* }
*
* On click, call:
*
* PainWikiConfirmations.confirm( treeId, muscleId, currentCounts )
* .then( function ( updated ) {
* currentCounts = updated;
* renderLearnPanel( updated );
* } )
* .catch( function ( err ) {
* if ( err === 'not-physician' ) {
* // show "verification required" message
* }
* } );
*/