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){