feat(v1.1): item 4 — minimal rubric (gauge + %) and rubric_debug 'why' toggle

- prefs API accepts the new rubric_debug key (off|on), the only new runtime
  write in v1.1; every section_state body now carries prefs.rubric_debug.
- fillRank renders the tank gauge + rounded % by default; the breakdown
  lines, audit footer and the quiet listing-enrichment fetch run only when
  the pref is on. List rows and the card face stay rubric-free (unchanged).
- Trends 'Actions' gains a 'Why breakdown: off | on' row in the setMode
  style; updateSectionUI repaints it on every section hydrate.
This commit is contained in:
Torres
2026-11-13 13:31:21 -04:00
parent e67f8a0cda
commit ac613e680a
2 changed files with 44 additions and 17 deletions

View File

@@ -456,6 +456,7 @@ def create_app(db_path, img_dir, state_dir=None):
for r in data['items']]
data['items'] = items
data['mode'] = mode
data['prefs'] = {'rubric_debug': get_pref(conn, 'rubric_debug', 'off')}
data['can_forge'] = (mode != 'strict'
or secmod.fresh_profile_exists(conn, sec))
data['counts'] = secmod.section_counts(conn, sec)
@@ -653,7 +654,8 @@ def create_app(db_path, img_dir, state_dir=None):
def set_pref_endpoint(body: dict):
key = body.get('key')
value = body.get('value')
allowed = {'forge_mode': ('ondemand', 'strict')}
allowed = {'forge_mode': ('ondemand', 'strict'),
'rubric_debug': ('off', 'on')}
if key not in allowed or value not in allowed[key]:
raise HTTPException(status_code=400,
detail='unknown or invalid pref')

View File

@@ -321,6 +321,7 @@ nav{height:calc(68px + env(safe-area-inset-bottom,0px));display:flex;align-items
<div class="tstatus" id="runStatus"></div>
</div>
<div class="trow"><span>Forge:</span><span id="tOnd" class="on">on demand</span><i>⇄</i><span id="tStr">strict</span></div>
<div class="trow"><span>Why breakdown:</span><span id="tRubOff" class="on">off</span><i>⇄</i><span id="tRubOn">on</span></div>
<div class="tmuted" id="tLastRun"></div>
</div>
</div>
@@ -805,7 +806,9 @@ function openDetail(data,ctx){
$('detail').classList.add('on'); $('dback').classList.add('on');
$('dbody').scrollTop=0;
}
/* P3 dossier rank block: gauge + points + strongest ± lines + audit footer */
/* P3 dossier rank block: gauge + points% minimal; the full "why" breakdown
and the audit footer render only when prefs.rubric_debug === 'on' (§10.5). */
function rubricOn(){ return !!(section&&section.prefs&&section.prefs.rubric_debug==='on'); }
function fillRank(data){
if(!data||data.tank==null||data.tank.value==null){
$('drank').style.display='none';
@@ -813,21 +816,27 @@ function fillRank(data){
}
$('drank').style.display='flex';
$('drankTank').innerHTML=tankSVG(data.tank,true);
$('dpts').textContent=data.tank.value+'/100';
const lines=(data.points_breakdown||[]).slice()
.sort((a,b)=>Math.abs(b.value)-Math.abs(a.value)).slice(0,4);
$('dlines').innerHTML=lines.map(l=>{
const lbl= l.source==='agent'?('agent: '+l.label):l.label;
const n= l.value>=0?(' +'+l.value):(' '+l.value);
return `<div class="${l.value>=0?'pos':'neg'}">· ${lbl}${n}</div>`;
}).join('');
const ri= section&&section.read_info;
$('dfoot').textContent= ri?`from ${ri.events} strikes · read ${relTime(ri.profile_at)}`:'';
// list/matches payloads carry no breakdown — fill it on the quiet
if(!data.points_breakdown&&data.id&&!legacyMode){
getJSON('/api/listing/'+data.id).then(full=>{
if(full&&full.points_breakdown&&dCtx) fillRank(full);
});
$('dpts').textContent=(data.tank.value|0)+'%';
if(rubricOn()){
const lines=(data.points_breakdown||[]).slice()
.sort((a,b)=>Math.abs(b.value)-Math.abs(a.value)).slice(0,4);
$('dlines').innerHTML=lines.map(l=>{
const lbl= l.source==='agent'?('agent: '+l.label):l.label;
const n= l.value>=0?(' +'+l.value):(' '+l.value);
return `<div class="${l.value>=0?'pos':'neg'}">· ${lbl}${n}</div>`;
}).join('');
const ri= section&&section.read_info;
$('dfoot').textContent= ri?`from ${ri.events} strikes · read ${relTime(ri.profile_at)}`:'';
// list/matches payloads carry no breakdown — fill it on the quiet
if(!data.points_breakdown&&data.id&&!legacyMode){
getJSON('/api/listing/'+data.id).then(full=>{
if(full&&full.points_breakdown&&dCtx) fillRank(full);
});
}
}else{
// minimal display: gauge + % only; skip the quiet enrichment fetch
$('dlines').innerHTML='';
$('dfoot').textContent='';
}
}
function closeDetail(){ const d=$('detail'); d.style.transition=''; d.style.transform=''; d.classList.remove('on'); $('dback').classList.remove('on'); dCtx=null; dEntry=null; }
@@ -1190,6 +1199,7 @@ function updateSectionUI(body){
sectDecided=decided;
paintSect();
paintMode();
paintRubricMode();
const c=body.counts||{};
$('repCounts').innerHTML=`♥ ${c.likes||0} · ★ ${c.maybes||0} · ✕ ${c.nopes||0}`;
const hits=[];
@@ -1203,6 +1213,21 @@ async function setMode(m){
}
$('modeOnd').onclick=()=>setMode('ondemand');
$('modeStr').onclick=()=>setMode('strict');
function paintRubricMode(){
const on=rubricOn();
$('tRubOff').classList.toggle('on',!on);
$('tRubOn').classList.toggle('on',on);
}
async function setRubricDebug(m){
const res=await fetch('/api/prefs',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({key:'rubric_debug',value:m})}).catch(()=>null);
if(res&&res.ok){
if(section){ section.prefs=section.prefs||{}; section.prefs.rubric_debug=m; }
paintRubricMode();
if(!legacyMode) loadSection(); // repaint any dossier already open
}
}
$('tRubOff').onclick=()=>setRubricDebug('off');
$('tRubOn').onclick=()=>setRubricDebug('on');
$('btnForge').onclick=async()=>{
if(forgeBusy||!section) return;
forgeBusy=true;