Connect the two tools: cross-nav links and Send to Scope Lock handoff

- Add a header link on each tool pointing to the other.
- Add a Send to Scope Lock button on Workshop clusters that carry a
  candidate solution tag. Writes the item to a shared localStorage
  queue (sde_scope_handoff_v1).
- Scope Lock drains that queue on load and adds each item to the
  Scope Boundary Board as unsorted, deduping by a deterministic id so
  a repeat send never creates a duplicate.
- No backend needed: both tools already share the same browser origin.
- Verified with a jsdom test covering send, drain, reload, and
  re-send-after-reset.
- Update README and CHANGELOG.
This commit is contained in:
2026-09-04 14:44:16 -07:00
parent 50424acad1
commit 6baf908cf9
4 changed files with 77 additions and 2 deletions

View File

@@ -24,7 +24,7 @@
header .brand span{color:#c6c6c6; font-weight:400;}
header .spacer{flex:1;}
.persist-note{font-size:12px; color:#c6c6c6;}
.hdr-btn{background:transparent; color:#fff; border:1px solid #6f6f6f; padding:7px 13px; font-size:13.5px;}
.hdr-btn{background:transparent; color:#fff; border:1px solid #6f6f6f; padding:7px 13px; font-size:13.5px; text-decoration:none; display:inline-block;}
.hdr-btn:hover{background:#353535;}
.hdr-btn.primary{background:var(--interactive); border-color:var(--interactive);}
.hdr-btn.primary:hover{background:var(--interactive-hover);}
@@ -181,6 +181,7 @@
<div class="brand"><strong>Project SDE</strong> <span>| Field Problem Workshop v5</span></div>
<div class="spacer"></div>
<span class="persist-note">State lives in this session. Save JSON before closing.</span>
<a class="hdr-btn" href="/tools/scope-lock-meeting-suite.html">Scope Lock Meeting Suite &rarr;</a>
<button class="hdr-btn" id="btnLoad">Load JSON</button>
<button class="hdr-btn" id="btnSave">Save JSON</button>
<button class="hdr-btn" id="btnReset">Reset</button>
@@ -388,6 +389,26 @@ function clusterById(id){ return S.clusters.find(c=>c.id===id); }
function membersOf(cid){ return S.problems.filter(p=>p.cluster===cid); }
function activeMembers(cid){ return S.problems.filter(p=>p.cluster===cid && p.type==='Problem'); }
/* ================= HANDOFF TO SCOPE LOCK ================= */
// Shared localStorage queue that the Scope Lock Meeting Suite reads on load.
// Both tools are served from the same origin, so this works with no backend.
const SCOPE_HANDOFF_KEY = 'sde_scope_handoff_v1';
function sendToScopeLock(c){
let queue = [];
try{
const raw = localStorage.getItem(SCOPE_HANDOFF_KEY);
if(raw) queue = JSON.parse(raw);
if(!Array.isArray(queue)) queue = [];
}catch(e){ queue = []; }
queue.push({
id: 'ws_' + c.id,
t: c.ws,
note: c.root || c.name || '',
sentAt: Date.now()
});
try{ localStorage.setItem(SCOPE_HANDOFF_KEY, JSON.stringify(queue)); }catch(e){}
}
document.querySelectorAll('nav button').forEach(b=>{
b.addEventListener('click', ()=>{
document.querySelectorAll('nav button').forEach(x=>x.classList.remove('active'));
@@ -474,6 +495,7 @@ function renderMap(){
<span class="cl-count">${mem.length} issue${mem.length===1?'':'s'}${c.ws?' · '+esc(c.ws):''}</span>
<span class="spacer"></span>
${c.promoted?'<span class="promoted-flag">✓ In Breadcrumbs</span>':`<button class="btn small ghost" data-cprom="${c.id}">Promote →</button>`}
${c.ws ? (c.sentToScope ? '<span class="promoted-flag">✓ Sent to Scope Lock</span>' : `<button class="btn small ghost" data-cscope="${c.id}">Send to Scope Lock →</button>`) : ''}
<div class="dot-ctl">
<button class="dot-btn" data-cdot="${c.id}" data-d="-1"></button>
<span class="dot-n">${c.dots}</span>
@@ -508,6 +530,15 @@ document.getElementById('panel-map').addEventListener('click', e=>{
}
return;
}
if(t.dataset.cscope){
const c = clusterById(t.dataset.cscope);
if(c && c.ws && !c.sentToScope){
sendToScopeLock(c);
c.sentToScope = true;
save(); renderMap(); toast('Sent "' + c.ws + '" to Scope Lock');
}
return;
}
if(t.dataset.cdel){
const c = clusterById(t.dataset.cdel);
if(c){

View File

@@ -36,7 +36,7 @@
header .meta{font-size:13px; color:#c6c6c6;}
.hdr-btn{
background:transparent; color:#fff; border:1px solid #6f6f6f;
padding:7px 14px; font-size:14px;
padding:7px 14px; font-size:14px; text-decoration:none; display:inline-block;
}
.hdr-btn:hover{background:#353535;}
.hdr-btn.primary{background:var(--interactive); border-color:var(--interactive);}
@@ -231,6 +231,7 @@
<div class="brand"><strong>Project SDE</strong> <span>| Scope Lock Meeting Suite</span></div>
<div class="spacer"></div>
<div class="meta">Micron Pilot | Innovation Team</div>
<a class="hdr-btn" href="/tools/field-problem-workshop.html">Field Problem Workshop &rarr;</a>
<button class="hdr-btn" id="btnReset">Reset all</button>
<button class="hdr-btn primary" id="btnExport">Export meeting summary</button>
</header>
@@ -783,6 +784,33 @@ function confirmModal(opts, onConfirm){
}
function renderAll(){ renderScope(); renderRank(); renderRegistry(); renderReady(); }
/* ================= HANDOFF FROM FIELD PROBLEM WORKSHOP ================= */
// Shared localStorage queue the Workshop's "Send to Scope Lock" button writes to.
// Both tools are served from the same origin, so this works with no backend.
const SCOPE_HANDOFF_KEY = 'sde_scope_handoff_v1';
function drainScopeHandoff(){
let queue = [];
try{
const raw = localStorage.getItem(SCOPE_HANDOFF_KEY);
if(raw) queue = JSON.parse(raw);
if(!Array.isArray(queue)) queue = [];
}catch(e){ queue = []; }
if(!queue.length) return;
let added = 0;
queue.forEach(item=>{
if(!item || !item.id || !item.t) return;
if(S.scope.some(s=>s.id===item.id)) return; // already added on a previous load
S.scope.push({id:item.id, t:item.t, b:'', lock:false});
added++;
});
try{ localStorage.removeItem(SCOPE_HANDOFF_KEY); }catch(e){}
if(added){
save();
toast(added===1 ? 'Added 1 item from Field Problem Workshop' : 'Added ' + added + ' items from Field Problem Workshop');
}
}
drainScopeHandoff();
renderAll();
</script>
</body>