Round-1 test feedback + smoke-test script

- API smoke test (server/smoketest.py): stdlib end-to-end check of health,
  projects, SOPs, WPs, the AWP issue gate (409 → 200), status, metrics,
  comments, and cascade delete. Referenced from DEPLOYMENT.md.

SOP config:
- Constraints: fix custom constraints never appearing — renderStandardConstraints
  no longer clobbers state.constraints; customs render in their own list with
  remove buttons; modal gains a free-text "Add" field.
- Sources: add column headers (Data Type / Location-Platform / URL / Notes);
  preset data types are now fixed labels, "Add Source" creates an editable
  custom row.
- Issuance strategy: add a tooltip + worked examples for each option.
- Remove the "Comment submitted" acknowledgement popup (home + suite); keep the
  commenter name between comments.

WP creator:
- Clearing the last open constraint now offers to mark the package Issued and
  scrolls to the status control.
- Form sections are collapsible (click a section heading to fold it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 11:04:46 -07:00
parent c64b5c8b49
commit e3ef3b0023
7 changed files with 334 additions and 23 deletions

View File

@@ -477,18 +477,45 @@ function removeRole(i){
renderOptionalRoles();
}
// Seed the standard 10 once; after that, render reflects state.constraints
// (checkbox = whether each standard one is active) and never clobbers customs.
let _constraintsSeeded = false;
function renderStandardConstraints(){
const container = document.getElementById('standard-constraints');
if(!_constraintsSeeded){
if(!state.constraints || !state.constraints.length){
state.constraints = STANDARD_10_CONSTRAINTS.map(c=>({...c}));
}
_constraintsSeeded = true;
}
const active = name => state.constraints.some(c=>c.name===name);
container.innerHTML = STANDARD_10_CONSTRAINTS.map(c=>`
<div style="display:flex; align-items:start; gap:0.75rem; padding:0.75rem; background:var(--bg); border:1px solid var(--border); border-radius:6px; margin-bottom:0.5rem;">
<input type="checkbox" id="const_${c.name}" checked onchange="toggleConstraint('${c.name}')" style="width:18px; height:18px; cursor:pointer; margin-top:0.2rem;">
<input type="checkbox" id="const_${c.name}" ${active(c.name)?'checked':''} onchange="toggleConstraint('${c.name}')" style="width:18px; height:18px; cursor:pointer; margin-top:0.2rem;">
<div style="flex:1;">
<label for="const_${c.name}" style="margin:0; font-weight:600; display:block; cursor:pointer;">${c.name}</label>
<div style="font-size:12px; color:var(--text-dim); margin-top:0.25rem;">${c.description}</div>
</div>
</div>
`).join('');
state.constraints = STANDARD_10_CONSTRAINTS.map(c=>({...c}));
renderCustomConstraints();
}
// Render the custom (non-standard) constraints into their own list with remove buttons.
function renderCustomConstraints(){
const el = document.getElementById('custom-constraints-list'); if(!el) return;
const stdNames = STANDARD_10_CONSTRAINTS.map(c=>c.name);
const customs = state.constraints.filter(c=>!stdNames.includes(c.name));
el.innerHTML = customs.length ? customs.map(c=>`
<div style="display:flex; align-items:center; justify-content:space-between; gap:0.75rem; padding:0.6rem 0.75rem; background:var(--bg); border:1px solid var(--border); border-radius:6px; margin-bottom:0.5rem;">
<strong>${escAttr(c.name)}</strong>
<button onclick="removeCustomConstraint('${c.name.replace(/'/g,"\\'")}')" title="Remove" style="background:var(--danger); color:#fff; border:none; border-radius:4px; width:28px; height:28px; cursor:pointer; font-weight:600;">✕</button>
</div>`).join('') : `<div style="font-size:12px; color:var(--text-dim);">No custom constraints added yet.</div>`;
}
function removeCustomConstraint(name){
state.constraints = state.constraints.filter(c=>c.name!==name);
renderCustomConstraints();
}
function toggleConstraint(name){
@@ -514,13 +541,24 @@ function closeConstraintModal(){
}
function addCustomConstraint(name){
if(!state.constraints.find(c=>c.name===name)){
if(name && !state.constraints.find(c=>c.name===name)){
state.constraints.push({name,description:''});
}
closeConstraintModal();
renderStandardConstraints();
}
// Free-text custom constraint from the modal's input.
function addCustomConstraintText(){
const inp = document.getElementById('custom-constraint-input');
const name = (inp && inp.value || '').trim();
if(!name){ if(inp) inp.focus(); return; }
if(state.constraints.find(c=>c.name===name)){ alert('That constraint is already in the list.'); return; }
state.constraints.push({name, description:''});
if(inp) inp.value='';
renderStandardConstraints();
}
const DEFAULT_SEQUENCE = ['Layout','Conduit Install','Tray Install','Wire Pull','Device Install','Termination','QC Inspection','Commissioning'];
let seqDragIndex = null;
@@ -600,20 +638,30 @@ const DEFAULT_SOURCES = [
function escAttr(v){ return String(v==null?'':v).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function renderSources(){
const container = document.getElementById('sources-list');
if(!state.sources.length) state.sources = DEFAULT_SOURCES.map(s=>({label:s.label, system:'', notes:'', link:'', ph:s.ph}));
container.innerHTML = state.sources.map((s,i)=>`
<div style="display:grid; grid-template-columns:150px 150px 250px 150px 30px; gap:1rem; align-items:center; padding:1rem; background:var(--bg); border-radius:6px; margin-bottom:0.5rem; border:1px solid var(--border);">
<input type="text" value="${escAttr(s.label)}" placeholder="Label" onchange="state.sources[${i}].label=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
<input type="text" value="${escAttr(s.system)}" placeholder="${escAttr(s.ph||'System of record')}" onchange="state.sources[${i}].system=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
<input type="text" value="${escAttr(s.link)}" placeholder="Paste SharePoint 'Copy Link' URL" onchange="state.sources[${i}].link=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
<input type="text" value="${escAttr(s.notes)}" placeholder="Notes" onchange="state.sources[${i}].notes=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
<button style="background:var(--danger); color:white; padding:0.25rem; width:30px; height:30px; border:none; border-radius:4px; cursor:pointer; font-weight:600;" onclick="state.sources.splice(${i},1); renderSources()">✕</button>
</div>
`).join('');
if(!state.sources.length) state.sources = DEFAULT_SOURCES.map(s=>({label:s.label, system:'', notes:'', link:'', ph:s.ph, preset:true}));
const grid = "display:grid; grid-template-columns:170px 170px 1fr 160px 30px; gap:1rem; align-items:center;";
const inStyle = "padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;";
const header = `<div style="${grid} padding:0 1rem 0.4rem; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.03em; color:var(--text-dim);">
<div>Data Type</div><div>Location / Platform</div><div>URL</div><div>Notes</div><div></div>
</div>`;
container.innerHTML = header + state.sources.map((s,i)=>{
// Preset data types are fixed labels; custom rows (Add Source) get an editable name.
const dataType = s.preset
? `<div style="font-weight:600; font-size:13px;">${escAttr(s.label)}</div>`
: `<input type="text" value="${escAttr(s.label)}" placeholder="Custom data type" onchange="state.sources[${i}].label=this.value" style="${inStyle} font-weight:600;">`;
return `<div style="${grid} padding:1rem; background:var(--bg); border-radius:6px; margin-bottom:0.5rem; border:1px solid var(--border);">
${dataType}
<input type="text" value="${escAttr(s.system)}" placeholder="${escAttr(s.ph||'Procore / Bluebeam / SharePoint…')}" onchange="state.sources[${i}].system=this.value" style="${inStyle}">
<input type="text" value="${escAttr(s.link)}" placeholder="Paste the 'Copy Link' URL" onchange="state.sources[${i}].link=this.value" style="${inStyle}">
<input type="text" value="${escAttr(s.notes)}" placeholder="Notes" onchange="state.sources[${i}].notes=this.value" style="${inStyle}">
<button style="background:var(--danger); color:white; padding:0.25rem; width:30px; height:30px; border:none; border-radius:4px; cursor:pointer; font-weight:600;" onclick="state.sources.splice(${i},1); renderSources()" title="Remove">✕</button>
</div>`;
}).join('');
}
function addSource(){
state.sources.push({label:'',system:'',notes:'',link:''});
// Added rows are custom — the user types their own data type here.
state.sources.push({label:'', system:'', notes:'', link:'', preset:false});
renderSources();
}
@@ -823,9 +871,7 @@ function submitComment(){
if(window.postFeedback) window.postFeedback({type:'sop_step_comment', ...comment});
document.getElementById('comment-text').value = '';
document.getElementById('commenter-name').value = '';
loadStepComments();
alert('✓ Comment submitted!');
}
function exportComments(){