From 6baf908cf9a9d9a1891b76bef996f309f614c12e Mon Sep 17 00:00:00 2001 From: Matt Mabrey Date: Fri, 4 Sep 2026 14:44:16 -0700 Subject: [PATCH] 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. --- CHANGELOG.md | 4 ++++ README.md | 12 +++++++++++ tools/field-problem-workshop.html | 33 ++++++++++++++++++++++++++++- tools/scope-lock-meeting-suite.html | 30 +++++++++++++++++++++++++- 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af343f8..4845352 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project are logged here, newest first. This file starts from the point the toolkit was handed off and set up in this repo. +## 2026-09-04 (2) + +- Added: the two tools now work together. Each tool's header has a link to the other tool. On the Field Problem Workshop, a cluster tagged with a candidate solution (for example "Tracking MVP (pilot)") gets a "Send to Scope Lock" button. Clicking it adds that cluster as a real, unsorted item on the Scope Lock Meeting Suite's Scope Boundary Board. Both tools already share the same browser origin, so this uses a small shared `localStorage` queue; no backend or database was needed. The item lands on Scope Lock's board the next time that tool is opened or refreshed, and re-sending the same cluster will not create a duplicate. Verified with a jsdom test that simulates both tools sharing one browser's storage. + ## 2026-09-04 - Fixed: Portainer stack deploy failed with `env file /data/compose/44/.env not found`. Cause: `docker-compose.yml` used `env_file: - .env`, which requires a real file on disk; Portainer's UI-entered environment variables satisfy `${VAR}` substitution in the compose file but do not create that file. Changed `docker-compose.yml` to reference each setting as `${VAR:-default}` under `environment:` instead, which works both for a real local `.env` (auto-loaded by Compose for substitution) and for values entered directly in Portainer. Corrected the wrong explanation of this in `PORTAINER_DEPLOY.md`. diff --git a/README.md b/README.md index d5998b3..168a189 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,18 @@ The two tools store data in different ways. 1. Click Export in the tool to create a durable record. 2. Save the exported file to your computer or to a shared drive. +## How the tools work together + +Each tool's header has a link to the other tool. + +The Field Problem Workshop can send a cluster to the Scope Lock Meeting Suite. + +1. Tag a cluster with a candidate solution, for example "Tracking MVP (pilot)". +2. Click "Send to Scope Lock" on that cluster. +3. Open the Scope Lock Meeting Suite. The cluster appears as a new, unsorted item on the Scope Boundary Board. + +The send action stores the item in the browser's local storage. Both tools must run from the same server address for this to work. Sending the same cluster twice does not create a duplicate item. + ## Reset a tool Each tool has a Reset control. diff --git a/tools/field-problem-workshop.html b/tools/field-problem-workshop.html index 1e89cc4..4319b4f 100644 --- a/tools/field-problem-workshop.html +++ b/tools/field-problem-workshop.html @@ -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 @@
Project SDE | Field Problem Workshop v5
State lives in this session. Save JSON before closing. + Scope Lock Meeting Suite → @@ -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(){ ${mem.length} issue${mem.length===1?'':'s'}${c.ws?' · '+esc(c.ws):''} ${c.promoted?'':``} + ${c.ws ? (c.sentToScope ? '' : ``) : ''}
${c.dots} @@ -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){ diff --git a/tools/scope-lock-meeting-suite.html b/tools/scope-lock-meeting-suite.html index 495aba7..d35e287 100644 --- a/tools/scope-lock-meeting-suite.html +++ b/tools/scope-lock-meeting-suite.html @@ -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 @@
Project SDE | Scope Lock Meeting Suite
Micron Pilot | Innovation Team
+ Field Problem Workshop → @@ -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();