diff --git a/mcp-server/search.html b/mcp-server/search.html
index 6d47d0c..9c4bcb6 100644
--- a/mcp-server/search.html
+++ b/mcp-server/search.html
@@ -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){