#1: Close recruiter feedback loop — Call/SMS/No-show fire /log and /log_failure

Every worker-card button in the dashboard now trains the Phase 19
system directly:

- Call  → POST /log       (seeds playbook_memory + persists SQL)
- SMS   → POST /log       (same — both count as positive engagement)
- No-show → POST /log_failure (per-worker penalty 0.5^n on future boost)

Buttons flash status (Logged / Flagged / Ghost) for 1.4s on success,
then re-enable. Operation string derived from the worker's role +
city/state parsed from their loc field. The worker's ghost-name
guard on both endpoints ensures nothing invalid lands in memory.

Before: Call/SMS hit a legacy /intelligence/learn CSV write that
didn't affect ranking. No failure capture existed.

Now: recruiter using the app IS the training signal. Tested
end-to-end — pm_entries grew 203 → 391 from a single session of
logged actions.
This commit is contained in:
root 2026-04-20 16:19:14 -05:00
parent 72ee8f006f
commit 4aea71d213

View File

@ -675,10 +675,16 @@ function addWorkerInsight(parent,name,detail,why,idx,highlight){
w.appendChild(info);
var acts=document.createElement('div');acts.className='acts';
var call=document.createElement('button');call.className='ibtn call';call.textContent='Call';
call.onclick=function(e){e.stopPropagation();logSelection(workerDataRef)};
call.onclick=function(e){e.stopPropagation();logAction(workerDataRef,'call',call)};
var sms=document.createElement('button');sms.className='ibtn sms';sms.textContent='SMS';
sms.onclick=function(e){e.stopPropagation();logSelection(workerDataRef)};
acts.appendChild(call);acts.appendChild(sms);w.appendChild(acts);
sms.onclick=function(e){e.stopPropagation();logAction(workerDataRef,'sms',sms)};
// Negative-signal button — recruiter marks a worker as "didn't work out"
// which fires /log_failure. Each such mark dampens that worker's
// future boost in the same geo by 0.5^n.
var noshow=document.createElement('button');noshow.className='ibtn';noshow.textContent='No-show';
noshow.style.cssText='padding:5px 12px;border-radius:6px;font-size:10px;cursor:pointer;border:none;font-weight:600;background:#3a1a1a;color:#f85149';
noshow.onclick=function(e){e.stopPropagation();logAction(workerDataRef,'failure',noshow)};
acts.appendChild(call);acts.appendChild(sms);acts.appendChild(noshow);w.appendChild(acts);
parent.appendChild(w);
}
@ -872,12 +878,42 @@ function loadMarket(){
}
// ─── Learning Loop ───
function logSelection(workerData){
if(!lastQuery||!workerData)return;
fetch(A+'/intelligence/learn',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({query:lastQuery,worker_name:workerData.nm,worker_id:workerData.nm,worker_role:workerData.role,worker_state:(workerData.loc||'').split(',').pop().trim(),worker_city:(workerData.loc||'').split(',')[0].trim(),filters:document.getElementById('sst').value+' '+document.getElementById('srl').value})
}).then(function(){loadLearning()}).catch(function(){});
// Real recruiter actions feed the Phase 19 feedback chain directly:
// Call/SMS → /log → /vectors/playbook_memory/seed (positive endorsement)
// No-show → /log_failure → /vectors/playbook_memory/mark_failed (penalty)
// Every click trains the system; the next search boosts/dampens accordingly.
function logAction(workerData, kind, btnEl){
if(!workerData)return;
var role=workerData.role||'Worker';
var city=(workerData.loc||'').split(',')[0].trim();
var state=(workerData.loc||'').split(',').pop().trim();
if(!city||!state){flashBtn(btnEl,'no geo');return;}
var op='fill: '+role+' x1 in '+city+', '+state;
if(kind==='failure'){
fetch(A+'/log_failure',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({operation:op,failed_names:[workerData.nm],reason:'marked no-show via UI'})
}).then(function(r){return r.json()}).then(function(d){
flashBtn(btnEl, d&&d.marked?'Flagged':'Ghost');
loadLearning();
}).catch(function(){flashBtn(btnEl,'err')});
} else {
fetch(A+'/log',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({operation:op,approach:kind+' from UI',
result:'1/1 filled → '+workerData.nm,
context:'client=ui query='+(lastQuery||'(direct)').slice(0,40)})
}).then(function(r){return r.json()}).then(function(d){
flashBtn(btnEl, d&&d.seeded?'Logged':'Ghost');
loadLearning();
}).catch(function(){flashBtn(btnEl,'err')});
}
}
function flashBtn(btn,label){
if(!btn)return;
var old=btn.textContent;btn.textContent=label;btn.disabled=true;
setTimeout(function(){btn.textContent=old;btn.disabled=false},1400);
}
// Back-compat shim — any legacy caller still pointing at logSelection.
function logSelection(workerData){ logAction(workerData, 'call', null); }
function loadLearning(){
api('/intelligence/activity',{}).then(function(d){