demo: console — sober worker cards (mirror dashboard styling)

J: "can you update Staffer's Console too the same look." Console
rendered worker rows in three places (Chapter 4 permit-contract
candidates, Chapter 8 triage backfills, Chapter 9 try-it-yourself
results) with the original 28px square avatar + flat backgrounds —
inconsistent with the new dashboard design.

Three changes:

1. CSS — .worker now has a 3px left-edge border that color-codes the
   role family, and .av is a 32px circle with a muted dark background
   + 1px ring + monogram initials. Five role-band colors mirror
   search.html: warehouse blue / production amber / trades purple /
   driver green / lead orange. Plus a .role-pill style matching the
   dashboard's small uppercase chip.

2. Helpers — added ROLE_BANDS regex table + roleBand() classifier and
   a new workerRow(name, role, detail, opts) builder. Same regex
   patterns as search.html so a "Forklift Operator" classifies
   identically on every page. opts.endorsed adds the green endorsed
   chip; opts.score appends a rank badge.

3. Replaced the three inline avatar+row constructors with workerRow()
   calls. Net: console.html lost ~20 lines of duplicated DOM building
   while gaining role bands + pills.

Verified end-to-end via playwright on devop.live/lakehouse/console:
  Chapter 8 triage scenario "Marcus running late site 4422":
    5 backfill rows render with [warehouse] band + WAREHOUSE pill +
    monogram avatars (SBC, ETW, SHC, WMG, MEB).
  Same sober look as the dashboard worker cards. No emojis, no
  cartoons, color-coded role family on the left edge.
This commit is contained in:
root 2026-04-27 23:47:12 -05:00
parent f92b55615f
commit cdf5f5926a

View File

@ -54,8 +54,19 @@ details .body{padding-top:10px;font-size:12px;color:#8b949e}
.accent-g{border-left:3px solid #3fb950} .accent-g{border-left:3px solid #3fb950}
.accent-r{border-left:3px solid #f85149} .accent-r{border-left:3px solid #f85149}
.worker{display:flex;align-items:center;gap:10px;padding:8px 10px;background:#161b22;border-radius:6px;margin-bottom:4px;font-size:12px} .worker{display:flex;align-items:center;gap:10px;padding:8px 10px;background:#161b22;border-radius:6px;margin-bottom:4px;font-size:12px;border-left:3px solid #30363d}
.worker .av{width:28px;height:28px;border-radius:6px;background:#1a2744;display:flex;align-items:center;justify-content:center;font-weight:600;color:#e6edf3;font-size:10px;flex-shrink:0} .worker .av{width:32px;height:32px;border-radius:50%;background:#0d1117;border:1px solid #21262d;display:flex;align-items:center;justify-content:center;font-weight:600;color:#c9d1d9;font-size:11px;flex-shrink:0;letter-spacing:0.5px}
.worker[data-role-band="warehouse"]{border-left-color:#58a6ff}
.worker[data-role-band="production"]{border-left-color:#d29922}
.worker[data-role-band="trades"]{border-left-color:#bc8cff}
.worker[data-role-band="driver"]{border-left-color:#3fb950}
.worker[data-role-band="lead"]{border-left-color:#f0883e}
.role-pill{display:inline-block;font-size:9px;padding:1px 7px;border-radius:3px;background:#0d1117;color:#8b949e;margin-right:6px;font-weight:600;letter-spacing:0.4px;text-transform:uppercase;border-left:2px solid #30363d;vertical-align:1px}
.role-pill[data-rb="warehouse"]{border-left-color:#58a6ff;color:#79c0ff}
.role-pill[data-rb="production"]{border-left-color:#d29922;color:#e3b341}
.role-pill[data-rb="trades"]{border-left-color:#bc8cff;color:#d2a8ff}
.role-pill[data-rb="driver"]{border-left-color:#3fb950;color:#56d364}
.role-pill[data-rb="lead"]{border-left-color:#f0883e;color:#ffa657}
.worker .info{flex:1;min-width:0} .worker .info{flex:1;min-width:0}
.worker .nm{color:#e6edf3;font-weight:500} .worker .nm{color:#e6edf3;font-weight:500}
.worker .why{color:#545d68;font-size:11px;margin-top:1px} .worker .why{color:#545d68;font-size:11px;margin-top:1px}
@ -199,6 +210,57 @@ var A=location.origin+P;
// DOM helpers — all dynamic content goes through these. No innerHTML // DOM helpers — all dynamic content goes through these. No innerHTML
// anywhere in the script; every API-derived string passes through // anywhere in the script; every API-derived string passes through
// textContent so no injection path regardless of upstream data. // textContent so no injection path regardless of upstream data.
// Role classification — mirrors search.html, no emojis. Maps role
// strings to a band+label used by the worker-card border + role pill.
var ROLE_BANDS = [
{ match: /forklift|warehouse|associate|material\s*handler|loader|loading|packag|shipping|logistics|inventory|sanitation|janit/i, band: 'warehouse', label: 'Warehouse' },
{ match: /production|assembl/i, band: 'production', label: 'Production' },
{ match: /welder|weld|electric|maint(enance)?\s*tech|cnc|machine\s*op|hvac|plumb|carpenter|mason/i, band: 'trades', label: 'Skilled Trade' },
{ match: /driver|truck|haul|cdl/i, band: 'driver', label: 'Driver' },
{ match: /line\s*lead|supervisor|foreman|coordinator/i, band: 'lead', label: 'Lead' },
{ match: /quality/i, band: 'production', label: 'Quality' },
];
function roleBand(role){
if(!role) return { band: 'warehouse', label: '' };
for (var i = 0; i < ROLE_BANDS.length; i++) {
if (ROLE_BANDS[i].match.test(role)) return ROLE_BANDS[i];
}
return { band: 'warehouse', label: role.split(' ')[0].toUpperCase().slice(0, 12) };
}
// Build a sober worker card: monogram avatar + colored role band on
// the left edge + uppercase role pill in the detail line. Used by
// every chapter that renders worker rows. `name` and `role` drive the
// classification; `detail` is the full text after the pill.
function workerRow(name, role, detail, opts){
opts = opts || {};
var band = roleBand(role||'');
var w = el('div','worker');
if(band.band) w.dataset.roleBand = band.band;
var initials = (name||'?').split(' ').map(function(s){return (s[0]||'').toUpperCase()}).join('').substring(0,2);
w.appendChild(el('div','av',initials));
var info = el('div','info');
var nm = el('div','nm', name||'?');
if(opts.endorsed){
nm.appendChild(el('span','boost-chip',opts.endorsed));
}
info.appendChild(nm);
var why = el('div','why');
if(band.label){
var pill = document.createElement('span'); pill.className='role-pill';
pill.dataset.rb = band.band;
pill.textContent = band.label;
why.appendChild(pill);
}
why.appendChild(document.createTextNode(detail||''));
info.appendChild(why);
w.appendChild(info);
if(opts.score){
w.appendChild(el('div','score', opts.score));
}
return w;
}
function el(tag, cls, text){ function el(tag, cls, text){
var e=document.createElement(tag); var e=document.createElement(tag);
if(cls) e.className=cls; if(cls) e.className=cls;
@ -380,21 +442,13 @@ function loadChapter4(){
var list=document.createElement('div');list.style.marginTop='6px'; var list=document.createElement('div');list.style.marginTop='6px';
(prop.candidates||[]).slice(0,5).forEach(function(cand,i){ (prop.candidates||[]).slice(0,5).forEach(function(cand,i){
var w=el('div','worker'); var detail = cand.doc_id+' · '+(cand.playbook_boost>0?'boosted +'+cand.playbook_boost.toFixed(3)+' by memory · ':'')+'semantic score '+(cand.score||0).toFixed(3);
var initials=(cand.name||'?').split(' ').map(function(s){return (s[0]||'').toUpperCase()}).join('').substring(0,2); var endorsed = (cand.playbook_boost||0) > 0
w.appendChild(el('div','av',initials)); ? 'Endorsed · '+((cand.playbook_citations||[]).length)+' past fill'+((cand.playbook_citations||[]).length!==1?'s':'')
var info=el('div','info'); : null;
var nm=el('div','nm',cand.name||cand.doc_id||'?'); list.appendChild(workerRow(cand.name||cand.doc_id||'?', prop.role||'', detail, {
if((cand.playbook_boost||0)>0){ endorsed: endorsed, score: '#'+(i+1)
var ncit=(cand.playbook_citations||[]).length; }));
nm.appendChild(el('span','boost-chip','Endorsed · '+ncit+' past fill'+(ncit!==1?'s':'')));
}
info.appendChild(nm);
var why=cand.doc_id+' · '+(cand.playbook_boost>0?'boosted +'+cand.playbook_boost.toFixed(3)+' by memory · ':'')+'semantic score '+(cand.score||0).toFixed(3);
info.appendChild(el('div','why',why));
w.appendChild(info);
w.appendChild(el('div','score','#'+(i+1)));
list.appendChild(w);
}); });
card.appendChild(list); card.appendChild(list);
@ -628,12 +682,8 @@ function loadChapter8(){
bfHdr.textContent='✓ '+d.backfills.length+' local '+(d.worker.role||'workers')+' available — sorted by responsiveness'; bfHdr.textContent='✓ '+d.backfills.length+' local '+(d.worker.role||'workers')+' available — sorted by responsiveness';
host.appendChild(bfHdr); host.appendChild(bfHdr);
d.backfills.slice(0,5).forEach(function(c){ d.backfills.slice(0,5).forEach(function(c){
var row=el('div','row'); var detail=(c.role||'?')+' · '+(c.city||'')+', '+(c.state||'')+' · rel '+Math.round((c.rel||0)*100)+'% · resp '+Math.round((c.resp||0)*100)+'%';
var left=document.createElement('div');left.style.flex='1';left.style.minWidth='0'; host.appendChild(workerRow(c.name||'?', c.role||'', detail));
left.appendChild(el('div','title',c.name));
left.appendChild(el('div','meta',(c.role||'?')+' · '+(c.city||'')+', '+(c.state||'')+' · rel '+Math.round((c.rel||0)*100)+'% · resp '+Math.round((c.resp||0)*100)+'%'));
row.appendChild(left);
host.appendChild(row);
}); });
} }
var narr=el('div','narr'); var narr=el('div','narr');
@ -675,23 +725,16 @@ function runTry(){
var workers=d.sql_results||d.vector_results||d.results||[]; var workers=d.sql_results||d.vector_results||d.results||[];
workers.slice(0,5).forEach(function(w,i){ workers.slice(0,5).forEach(function(w,i){
var row=el('div','worker');
var nm=w.name||(w.text||'').split('—')[0].trim()||w.doc_id||'?'; var nm=w.name||(w.text||'').split('—')[0].trim()||w.doc_id||'?';
var initials=nm.split(' ').map(function(s){return (s[0]||'').toUpperCase()}).join('').substring(0,2);
row.appendChild(el('div','av',initials));
var info=el('div','info');
var n=el('div','nm',nm);
if((w.playbook_boost||0)>0){
n.appendChild(el('span','boost-chip','Endorsed · '+((w.playbook_citations||[]).length||'?')+' past fill(s)'));
}
info.appendChild(n);
var bits=[]; var bits=[];
if(w.role) bits.push(w.role); if(w.role) bits.push(w.role);
if(w.city&&w.state) bits.push(w.city+', '+w.state); if(w.city&&w.state) bits.push(w.city+', '+w.state);
if(w.rel!==undefined) bits.push('reliability '+Math.round(w.rel*100)+'%'); if(w.rel!==undefined) bits.push('reliability '+Math.round(w.rel*100)+'%');
if(w.avail!==undefined) bits.push('availability '+Math.round(w.avail*100)+'%'); if(w.avail!==undefined) bits.push('availability '+Math.round(w.avail*100)+'%');
info.appendChild(el('div','why',bits.join(' · ')||'AI semantic match')); var endorsed = (w.playbook_boost||0) > 0
row.appendChild(info); ? 'Endorsed · '+((w.playbook_citations||[]).length||'?')+' past fill(s)'
: null;
var row = workerRow(nm, w.role||'', bits.join(' · ')||'AI semantic match', { endorsed: endorsed });
row.appendChild(el('div','score','#'+(i+1))); row.appendChild(el('div','score','#'+(i+1)));
card.appendChild(row); card.appendChild(row);
}); });