<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-GB">
	<id>https://painwiki.com/wiki/index.php?action=history&amp;feed=atom&amp;title=MediaWiki%3AGadget-ConfirmationStore.js</id>
	<title>MediaWiki:Gadget-ConfirmationStore.js - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://painwiki.com/wiki/index.php?action=history&amp;feed=atom&amp;title=MediaWiki%3AGadget-ConfirmationStore.js"/>
	<link rel="alternate" type="text/html" href="https://painwiki.com/wiki/index.php?title=MediaWiki:Gadget-ConfirmationStore.js&amp;action=history"/>
	<updated>2026-10-19T18:09:47Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.45.3</generator>
	<entry>
		<id>https://painwiki.com/wiki/index.php?title=MediaWiki:Gadget-ConfirmationStore.js&amp;diff=312&amp;oldid=prev</id>
		<title>Yatreyu: Created page with &quot;/**  * 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/&lt;TreeId&gt;  * Format:  { muscle_id: { n, last, sessions: [{ts, actor}] } }  */  ( function ( mw, $ ) { &#039;use strict&#039;;  const MAX_SESSIONS_PER_MUSCLE = 50; const DATA_PAGE_PREF...&quot;</title>
		<link rel="alternate" type="text/html" href="https://painwiki.com/wiki/index.php?title=MediaWiki:Gadget-ConfirmationStore.js&amp;diff=312&amp;oldid=prev"/>
		<updated>2026-05-22T23:54:30Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;&lt;span class=&quot;autocomment&quot;&gt;*  * 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/&amp;lt;TreeId&amp;gt;  * Format:  { muscle_id: { n, last, sessions: &amp;#91;{ts, actor}&amp;#93; } }: &lt;/span&gt;  ( function ( mw, $ ) { &amp;#039;use strict&amp;#039;;  const MAX_SESSIONS_PER_MUSCLE = 50; const DATA_PAGE_PREF...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;/**&lt;br /&gt;
 * DiagnosticTree — Confirmation Store v2&lt;br /&gt;
 *&lt;br /&gt;
 * Changes from v1:&lt;br /&gt;
 * - Anonymous confirmation disabled entirely&lt;br /&gt;
 * - Actor is always a username (physician group required)&lt;br /&gt;
 * - MAX_SESSIONS_PER_MUSCLE increased to 50&lt;br /&gt;
 * - isPhysician() helper exposed for UI gating&lt;br /&gt;
 *&lt;br /&gt;
 * Storage: Data:Confirmations/&amp;lt;TreeId&amp;gt;&lt;br /&gt;
 * Format:  { muscle_id: { n, last, sessions: [{ts, actor}] } }&lt;br /&gt;
 */&lt;br /&gt;
&lt;br /&gt;
( function ( mw, $ ) {&lt;br /&gt;
&amp;#039;use strict&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
const MAX_SESSIONS_PER_MUSCLE = 50;&lt;br /&gt;
const DATA_PAGE_PREFIX        = &amp;#039;Data:Confirmations/&amp;#039;;&lt;br /&gt;
const LS_KEY_PREFIX           = &amp;#039;painwiki_confirmations_&amp;#039;;&lt;br /&gt;
const REQUIRED_GROUP          = &amp;#039;physician&amp;#039;;&lt;br /&gt;
&lt;br /&gt;
// ── Access control ────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Returns true if the current user is in the &amp;#039;physician&amp;#039; group.&lt;br /&gt;
 * Used to gate the confirm button in the UI.&lt;br /&gt;
 */&lt;br /&gt;
function isPhysician() {&lt;br /&gt;
    const groups = mw.config.get( &amp;#039;wgUserGroups&amp;#039; ) || [];&lt;br /&gt;
    return groups.indexOf( REQUIRED_GROUP ) !== -1;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Returns true if the user is logged in at all.&lt;br /&gt;
 */&lt;br /&gt;
function isLoggedIn() {&lt;br /&gt;
    return !!mw.config.get( &amp;#039;wgUserName&amp;#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Returns the current username, or null if not logged in.&lt;br /&gt;
 */&lt;br /&gt;
function getActor() {&lt;br /&gt;
    return mw.config.get( &amp;#039;wgUserName&amp;#039; ) || null;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// ── Timestamp ─────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
function nowISO() {&lt;br /&gt;
    return new Date().toISOString();&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// ── Read ──────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
function loadConfirmations( treeId ) {&lt;br /&gt;
    const api   = new mw.Api();&lt;br /&gt;
    const lsKey = LS_KEY_PREFIX + treeId;&lt;br /&gt;
&lt;br /&gt;
    return api.get( {&lt;br /&gt;
        action:        &amp;#039;query&amp;#039;,&lt;br /&gt;
        prop:          &amp;#039;revisions&amp;#039;,&lt;br /&gt;
        titles:        DATA_PAGE_PREFIX + treeId,&lt;br /&gt;
        rvprop:        &amp;#039;content&amp;#039;,&lt;br /&gt;
        rvslots:       &amp;#039;main&amp;#039;,&lt;br /&gt;
        formatversion: 2&lt;br /&gt;
    } ).then( function ( data ) {&lt;br /&gt;
        const pages = data.query &amp;amp;&amp;amp; data.query.pages;&lt;br /&gt;
        if ( !pages || !pages.length ) return localFallback( lsKey );&lt;br /&gt;
        const page = pages[0];&lt;br /&gt;
        if ( page.missing ) return localFallback( lsKey );&lt;br /&gt;
        const content = page.revisions &amp;amp;&amp;amp;&lt;br /&gt;
                        page.revisions[0] &amp;amp;&amp;amp;&lt;br /&gt;
                        page.revisions[0].slots &amp;amp;&amp;amp;&lt;br /&gt;
                        page.revisions[0].slots.main &amp;amp;&amp;amp;&lt;br /&gt;
                        page.revisions[0].slots.main.content;&lt;br /&gt;
        if ( !content ) return localFallback( lsKey );&lt;br /&gt;
        try {&lt;br /&gt;
            const parsed = JSON.parse( content );&lt;br /&gt;
            localStorage.setItem( lsKey, JSON.stringify( parsed ) );&lt;br /&gt;
            return parsed;&lt;br /&gt;
        } catch ( e ) {&lt;br /&gt;
            return localFallback( lsKey );&lt;br /&gt;
        }&lt;br /&gt;
    } ).catch( function () {&lt;br /&gt;
        return localFallback( lsKey );&lt;br /&gt;
    } );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
function localFallback( lsKey ) {&lt;br /&gt;
    try {&lt;br /&gt;
        const raw = localStorage.getItem( lsKey );&lt;br /&gt;
        if ( raw ) return JSON.parse( raw );&lt;br /&gt;
    } catch ( e ) {}&lt;br /&gt;
    return {};&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// ── Write ─────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
/**&lt;br /&gt;
 * Records a confirmation.&lt;br /&gt;
 * Rejects immediately if the user is not in the physician group.&lt;br /&gt;
 *&lt;br /&gt;
 * @returns Promise resolving to updated counts, or rejecting with&lt;br /&gt;
 *          &amp;#039;not-physician&amp;#039; | &amp;#039;not-logged-in&amp;#039; error strings.&lt;br /&gt;
 */&lt;br /&gt;
function confirmMuscle( treeId, muscleId, currentCounts ) {&lt;br /&gt;
    if ( !isLoggedIn() ) {&lt;br /&gt;
        return Promise.reject( &amp;#039;not-logged-in&amp;#039; );&lt;br /&gt;
    }&lt;br /&gt;
    if ( !isPhysician() ) {&lt;br /&gt;
        return Promise.reject( &amp;#039;not-physician&amp;#039; );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    const actor  = getActor();&lt;br /&gt;
    const counts = JSON.parse( JSON.stringify( currentCounts ) );&lt;br /&gt;
&lt;br /&gt;
    if ( !counts[muscleId] ) {&lt;br /&gt;
        counts[muscleId] = { n: 0, last: null, sessions: [] };&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    const entry   = counts[muscleId];&lt;br /&gt;
    const ts      = nowISO();&lt;br /&gt;
&lt;br /&gt;
    entry.n    += 1;&lt;br /&gt;
    entry.last  = ts;&lt;br /&gt;
    entry.sessions.push( { ts: ts, actor: actor } );&lt;br /&gt;
&lt;br /&gt;
    // Enforce rolling window&lt;br /&gt;
    if ( entry.sessions.length &amp;gt; MAX_SESSIONS_PER_MUSCLE ) {&lt;br /&gt;
        entry.sessions = entry.sessions.slice(&lt;br /&gt;
            entry.sessions.length - MAX_SESSIONS_PER_MUSCLE&lt;br /&gt;
        );&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    return writeConfirmations( treeId, counts ).then( function () {&lt;br /&gt;
        // Also update local cache&lt;br /&gt;
        localStorage.setItem(&lt;br /&gt;
            LS_KEY_PREFIX + treeId,&lt;br /&gt;
            JSON.stringify( counts )&lt;br /&gt;
        );&lt;br /&gt;
        return counts;&lt;br /&gt;
    } ).catch( function ( err ) {&lt;br /&gt;
        // Write failed (network, permissions) — update local cache only&lt;br /&gt;
        localStorage.setItem(&lt;br /&gt;
            LS_KEY_PREFIX + treeId,&lt;br /&gt;
            JSON.stringify( counts )&lt;br /&gt;
        );&lt;br /&gt;
        mw.log.warn( &amp;#039;PainWiki: confirmation write failed —&amp;#039;, err );&lt;br /&gt;
        return counts;&lt;br /&gt;
    } );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
function writeConfirmations( treeId, counts ) {&lt;br /&gt;
    const api = new mw.Api();&lt;br /&gt;
    return api.postWithToken( &amp;#039;csrf&amp;#039;, {&lt;br /&gt;
        action:       &amp;#039;edit&amp;#039;,&lt;br /&gt;
        title:        DATA_PAGE_PREFIX + treeId,&lt;br /&gt;
        text:         JSON.stringify( counts, null, 2 ),&lt;br /&gt;
        summary:      &amp;#039;Confirmation: &amp;#039; + mw.config.get( &amp;#039;wgUserName&amp;#039; ),&lt;br /&gt;
        minor:        true,&lt;br /&gt;
        contentmodel: &amp;#039;json&amp;#039;,&lt;br /&gt;
        formatversion: 2&lt;br /&gt;
    } );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// ── Priors ────────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
function computeUpdatedPriors( schemaMuscles, counts, learningRate ) {&lt;br /&gt;
    learningRate = learningRate || 0.02;&lt;br /&gt;
    const ids = Object.keys( schemaMuscles );&lt;br /&gt;
    const p   = {};&lt;br /&gt;
&lt;br /&gt;
    ids.forEach( function ( mid ) {&lt;br /&gt;
        const base = schemaMuscles[mid].prior || 0.1;&lt;br /&gt;
        const n    = ( counts[mid] &amp;amp;&amp;amp; counts[mid].n ) || 0;&lt;br /&gt;
        p[mid]     = base + n * learningRate;&lt;br /&gt;
    } );&lt;br /&gt;
&lt;br /&gt;
    let total = 0;&lt;br /&gt;
    ids.forEach( function ( mid ) { total += p[mid]; } );&lt;br /&gt;
    ids.forEach( function ( mid ) { p[mid] /= total; } );&lt;br /&gt;
    return p;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// ── Public API ────────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
window.PainWikiConfirmations = {&lt;br /&gt;
    load:           loadConfirmations,&lt;br /&gt;
    confirm:        confirmMuscle,&lt;br /&gt;
    computePriors:  computeUpdatedPriors,&lt;br /&gt;
    isPhysician:    isPhysician,&lt;br /&gt;
    isLoggedIn:     isLoggedIn,&lt;br /&gt;
    MAX_SESSIONS:   MAX_SESSIONS_PER_MUSCLE&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
} )( mediaWiki, jQuery );&lt;br /&gt;
&lt;br /&gt;
/*&lt;br /&gt;
 * ── UI integration notes ───────────────────────────────────────────&lt;br /&gt;
 *&lt;br /&gt;
 * In DiagnosticTree.js, gate the confirm button on render:&lt;br /&gt;
 *&lt;br /&gt;
 *   const canConfirm = PainWikiConfirmations.isPhysician();&lt;br /&gt;
 *&lt;br /&gt;
 *   // If physician: render normal confirm button&lt;br /&gt;
 *   // If logged in but not physician: render greyed button with tooltip&lt;br /&gt;
 *   //   &amp;quot;Physician verification required — contact site admin&amp;quot;&lt;br /&gt;
 *   // If not logged in: render login prompt link instead of button&lt;br /&gt;
 *&lt;br /&gt;
 * Example:&lt;br /&gt;
 *&lt;br /&gt;
 *   function confirmButtonHTML( muscleId, label ) {&lt;br /&gt;
 *       if ( !PainWikiConfirmations.isLoggedIn() ) {&lt;br /&gt;
 *           return &amp;#039;&amp;lt;a href=&amp;quot;/wiki/Special:UserLogin&amp;quot; class=&amp;quot;dt-confirm-btn dt-confirm-login&amp;quot;&amp;gt;&amp;#039; +&lt;br /&gt;
 *                  &amp;#039;Log in to confirm&amp;lt;/a&amp;gt;&amp;#039;;&lt;br /&gt;
 *       }&lt;br /&gt;
 *       if ( !PainWikiConfirmations.isPhysician() ) {&lt;br /&gt;
 *           return &amp;#039;&amp;lt;button class=&amp;quot;dt-confirm-btn dt-confirm-btn--disabled&amp;quot; disabled &amp;#039; +&lt;br /&gt;
 *                  &amp;#039;title=&amp;quot;Physician verification required&amp;quot;&amp;gt;&amp;#039; +&lt;br /&gt;
 *                  &amp;#039;&amp;amp;#128274; Physician access required&amp;lt;/button&amp;gt;&amp;#039;;&lt;br /&gt;
 *       }&lt;br /&gt;
 *       return &amp;#039;&amp;lt;button class=&amp;quot;dt-confirm-btn&amp;quot; data-muscle=&amp;quot;&amp;#039; + muscleId + &amp;#039;&amp;quot; &amp;#039; +&lt;br /&gt;
 *              &amp;#039;data-label=&amp;quot;&amp;#039; + encodeURIComponent(label) + &amp;#039;&amp;quot;&amp;gt;&amp;amp;#10003; Confirm&amp;lt;/button&amp;gt;&amp;#039;;&lt;br /&gt;
 *   }&lt;br /&gt;
 *&lt;br /&gt;
 * On click, call:&lt;br /&gt;
 *&lt;br /&gt;
 *   PainWikiConfirmations.confirm( treeId, muscleId, currentCounts )&lt;br /&gt;
 *       .then( function ( updated ) {&lt;br /&gt;
 *           currentCounts = updated;&lt;br /&gt;
 *           renderLearnPanel( updated );&lt;br /&gt;
 *       } )&lt;br /&gt;
 *       .catch( function ( err ) {&lt;br /&gt;
 *           if ( err === &amp;#039;not-physician&amp;#039; ) {&lt;br /&gt;
 *               // show &amp;quot;verification required&amp;quot; message&lt;br /&gt;
 *           }&lt;br /&gt;
 *       } );&lt;br /&gt;
 */&lt;/div&gt;</summary>
		<author><name>Yatreyu</name></author>
	</entry>
</feed>