T9.3 - S6: one icon system - monochrome text glyphs, one meaning each
The set mixed colour emoji with dingbats, and the same glyph read as two things partly BECAUSE emoji render as per-platform artwork. The system chosen: monochrome text-presentation glyphs - the suite is classic-script vanilla HTML with no bundler, so an SVG sprite or icon font is a new asset pipeline, while text glyphs render through the same font stack as the words beside them. The enforceable form of "renders identically on Windows, macOS and a tablet": no emoji-range codepoint and no U+FE0F selector anywhere in UI source, swept by the probe on every run. Converted: green-check/red-cross emoji in the admin and users consoles to checkmark/cross, no-entry to circled-slash (blocked/on hold), the lock to the pencil already meaning "edit with a logged reason" on sign-offs, the star to the diamond, the folder to the reference marker, the side nav's lightning to the gear, and the WATCH glyph (U+231A - emoji-presentation BY DEFAULT per Unicode) to a text-presentation clock face. Dropped where the label already carried the meaning: lightning on Save & view, the camera on Add photo, the page/frame pictograms on file rows (the filename is the label). Stale help copy fixed while its emoji left: it still described the pre-T9.4 "Load sample" and the pre-T7.10 "Usage Logs" locations. The meaning-to-icon mapping is in docs/reference/tokens.md - one meaning per glyph, one glyph per meaning, both directions asserted from the document itself; the probe also sweeps every page for glyphs not in the approved set, so an unmapped icon cannot creep in. Verification (each probe run alone): NEW tests/icon_check.py 5/5. Regressions: frame_check 38/38, files_check 36/36, a11y_check 22/22, cards_check 44/44. Items: S6 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,13 +36,13 @@ async function checkHealth(){
|
||||
b.className='banner'; b.textContent='Checking…';
|
||||
const { status, json } = await api('GET','/api/health');
|
||||
if(status===200 && json && json.ok){
|
||||
b.className='banner ok'; b.textContent='✅ API reachable — /api/health returned ok.';
|
||||
b.className='banner ok'; b.textContent='✓ API reachable — /api/health returned ok.';
|
||||
} else if(status===404){
|
||||
b.className='banner bad'; b.textContent='❌ /api/ returns 404 — the reverse proxy is not routing /api/ to the API. The site loads but the API is unreachable from the browser.';
|
||||
b.className='banner bad'; b.textContent='✕ /api/ returns 404 — the reverse proxy is not routing /api/ to the API. The site loads but the API is unreachable from the browser.';
|
||||
} else if(status===0){
|
||||
b.className='banner bad'; b.textContent='❌ Could not reach the server: '+json;
|
||||
b.className='banner bad'; b.textContent='✕ Could not reach the server: '+json;
|
||||
} else {
|
||||
b.className='banner bad'; b.textContent='❌ Unexpected response: HTTP '+status;
|
||||
b.className='banner bad'; b.textContent='✕ Unexpected response: HTTP '+status;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,9 +121,9 @@ function stdConstraints(open){ return ['Safety & Permitting','Quality Control /
|
||||
async function seedDemo(){
|
||||
const o=document.getElementById('demo-out'); o.innerHTML='';
|
||||
let r = await api('GET','/api/health');
|
||||
if(!(r.status===200 && r.json && r.json.ok)){ demoLog('❌ API unreachable — fix /api/ routing first.'); return; }
|
||||
if(!(r.status===200 && r.json && r.json.ok)){ demoLog('✕ API unreachable — fix /api/ routing first.'); return; }
|
||||
r = await api('POST','/api/projects',{name:'DEMO — Micron INC (test data)',number:'DEMO-001',client:'Micron Technology, Inc.',division:'Semiconductor',site:'Boise, ID — Fab',created_by:'admin-console'});
|
||||
if(r.status!==200){ demoLog('❌ create project failed (HTTP '+r.status+')'); return; }
|
||||
if(r.status!==200){ demoLog('✕ create project failed (HTTP '+r.status+')'); return; }
|
||||
const pid=r.json.id; demoLog('Project created: '+r.json.name);
|
||||
r = await api('POST','/api/sops',{project_id:pid,name:'DEMO SOP',number:'DEMO-001',complete:true,data:{governance:{woFormat:'WP##-[Sector]-[TYPE]',disciplines:['Mechanical','Electrical','Tech'],discMode:'choice',instanceSuffix:'letter',woSize:'Standard — 3–5 days (≈40–80 hrs)',sizeHoursMax:'80'}}});
|
||||
const sid=r.json && r.json.id; demoLog('SOP created (complete).');
|
||||
@@ -142,7 +142,7 @@ async function seedDemo(){
|
||||
await mk('WP05-3P-PANEL','3P panel install','Panel Install','Draft',{disciplines:['Electrical'],hours:'120',constraints:stdConstraints(['Schedule']),due:'2026-07-20'});
|
||||
r = await api('GET','/api/wps/metrics?project_id='+pid);
|
||||
demoLog('\nMetrics (masters excluded): '+JSON.stringify(r.json));
|
||||
demoLog('\n✅ Done — "DEMO — Micron INC (test data)" now appears in the home picker.');
|
||||
demoLog('\n✓ Done — "DEMO — Micron INC (test data)" now appears in the home picker.');
|
||||
snapshot();
|
||||
}
|
||||
async function cleanDemo(){
|
||||
@@ -151,11 +151,11 @@ async function cleanDemo(){
|
||||
// archived=all, or an archived DEMO-/SMOKE- project becomes unreachable from
|
||||
// this button — the default list hides it and nothing else here can delete it.
|
||||
const r = await api('GET','/api/projects?archived=all');
|
||||
if(r.status!==200){ demoLog('❌ API unreachable (HTTP '+r.status+').'); return; }
|
||||
if(r.status!==200){ demoLog('✕ API unreachable (HTTP '+r.status+').'); return; }
|
||||
const targets=(r.json||[]).filter(p=>/^(DEMO-|SMOKE-)/.test(String(p.number||'')));
|
||||
if(!targets.length){ demoLog('Nothing to remove.'); return; }
|
||||
for(const p of targets){ await api('DELETE','/api/projects/'+p.id); demoLog('Deleted: '+p.name+' ('+p.number+')'); }
|
||||
demoLog('\n✅ Removed '+targets.length+' project(s).');
|
||||
demoLog('\n✓ Removed '+targets.length+' project(s).');
|
||||
snapshot();
|
||||
}
|
||||
|
||||
@@ -175,14 +175,14 @@ async function loadProjects(){
|
||||
const { status, json } = await api('GET','/api/projects?archived=all');
|
||||
if(status===403){
|
||||
banner.className='banner bad';
|
||||
banner.textContent='❌ Your account is not an admin, so you can’t archive or delete projects here.';
|
||||
banner.textContent='✕ Your account is not an admin, so you can’t archive or delete projects here.';
|
||||
wrap.innerHTML=''; return;
|
||||
}
|
||||
if(status===401){
|
||||
banner.className='banner bad'; banner.textContent='❌ Not signed in. Reload and log in again.'; wrap.innerHTML=''; return;
|
||||
banner.className='banner bad'; banner.textContent='✕ Not signed in. Reload and log in again.'; wrap.innerHTML=''; return;
|
||||
}
|
||||
if(status!==200 || !Array.isArray(json)){
|
||||
banner.className='banner bad'; banner.textContent='❌ Could not load projects (HTTP '+status+').'; wrap.innerHTML=''; return;
|
||||
banner.className='banner bad'; banner.textContent='✕ Could not load projects (HTTP '+status+').'; wrap.innerHTML=''; return;
|
||||
}
|
||||
banner.style.display='none';
|
||||
_adminProjects = json;
|
||||
@@ -284,14 +284,14 @@ async function loadDefaultMembers(){
|
||||
const { status, json } = await api('GET','/api/auth/users');
|
||||
if(status===403){
|
||||
banner.className='banner bad';
|
||||
banner.textContent='❌ Your account is not an admin, so you can’t change who is added to new projects.';
|
||||
banner.textContent='✕ Your account is not an admin, so you can’t change who is added to new projects.';
|
||||
wrap.innerHTML=''; return;
|
||||
}
|
||||
if(status===401){
|
||||
banner.className='banner bad'; banner.textContent='❌ Not signed in. Reload and log in again.'; wrap.innerHTML=''; return;
|
||||
banner.className='banner bad'; banner.textContent='✕ Not signed in. Reload and log in again.'; wrap.innerHTML=''; return;
|
||||
}
|
||||
if(status!==200 || !Array.isArray(json)){
|
||||
banner.className='banner bad'; banner.textContent='❌ Could not load users (HTTP '+status+').'; wrap.innerHTML=''; return;
|
||||
banner.className='banner bad'; banner.textContent='✕ Could not load users (HTTP '+status+').'; wrap.innerHTML=''; return;
|
||||
}
|
||||
banner.style.display='none';
|
||||
_defMemUsers = json;
|
||||
@@ -560,7 +560,7 @@ async function saveLocalization(){
|
||||
const m = document.getElementById('l10n-msg');
|
||||
if(m){ m.textContent = 'Saved.'; m.style.color = 'var(--green)'; }
|
||||
} else {
|
||||
msg.textContent = '❌ '+((json && json.detail) || ('HTTP '+status));
|
||||
msg.textContent = '✕ '+((json && json.detail) || ('HTTP '+status));
|
||||
msg.style.color = 'var(--red)';
|
||||
}
|
||||
}
|
||||
@@ -627,8 +627,8 @@ async function saveSettings(){
|
||||
async function testEmail(){
|
||||
const msg = document.getElementById('set-msg'); msg.textContent = 'Sending test…'; msg.style.color = 'var(--muted)';
|
||||
const { status, json } = await api('POST','/api/settings/test-email', {});
|
||||
if(status===200) { msg.textContent = '✅ Test sent to '+((json&&json.to)||'you')+'.'; msg.style.color = 'var(--green)'; }
|
||||
else { msg.textContent = '❌ '+((json && json.detail) || ('HTTP '+status)); msg.style.color = 'var(--red)'; }
|
||||
if(status===200) { msg.textContent = '✓ Test sent to '+((json&&json.to)||'you')+'.'; msg.style.color = 'var(--green)'; }
|
||||
else { msg.textContent = '✕ '+((json && json.detail) || ('HTTP '+status)); msg.style.color = 'var(--red)'; }
|
||||
}
|
||||
async function loadNotifications(){
|
||||
const box = document.getElementById('notif-box'); if(!box) return;
|
||||
|
||||
@@ -149,11 +149,11 @@ function renderDetail() {
|
||||
(((p.files) || []).length ? '<div class="fld-sec"><h3>Drawings</h3>' +
|
||||
p.files.map(function (f) {
|
||||
return '<a class="fld-drawing" href="/api/files/' + esc(f.id) + '" target="_blank" rel="noopener">' +
|
||||
'📄 ' + esc(f.name || 'drawing') + (f.description ? ' — ' + esc(f.description) : '') + '</a>';
|
||||
'' + esc(f.name || 'drawing') + (f.description ? ' — ' + esc(f.description) : '') + '</a>';
|
||||
}).join('') + '</div>' : '') +
|
||||
'<div class="fld-sec"><h3>Add field update</h3>' +
|
||||
'<textarea class="fld-note" id="fld-note" placeholder="What happened on site? (progress, blockers, notes)" oninput="draftNote=this.value">' + esc(draftNote) + '</textarea>' +
|
||||
'<div class="fld-photo-row"><label class="fld-btn">📷 Add photo<input type="file" accept="image/*" capture="environment" style="display:none" onchange="onPhoto(event)"></label>' +
|
||||
'<div class="fld-photo-row"><label class="fld-btn">Add photo<input type="file" accept="image/*" capture="environment" style="display:none" onchange="onPhoto(event)"></label>' +
|
||||
'<span id="photo-status" style="font-size:13px;color:var(--cds-text-secondary)">' + (pendingPhoto ? 'Photo attached ✓' : '') + '</span></div>' +
|
||||
'<div style="margin-top:12px"><button class="fld-btn primary" onclick="addUpdate()">Add to log</button></div>' +
|
||||
'</div>' +
|
||||
|
||||
18
html/help.js
18
html/help.js
@@ -93,7 +93,7 @@
|
||||
<li><strong>Dashboard</strong> — track status, hours, due dates, and what's gating each package across the project.</li>
|
||||
</ol>
|
||||
<h4>Moving around</h4>
|
||||
<p>From the home page, open <strong>SOP Configuration</strong>, the <strong>Work Package Creator</strong>, or the <strong>Dashboard</strong>. Inside the suite, switch any time using the top tabs: <strong>⚙️ SOP Configuration</strong>, <strong>📋 Work Package Creation</strong>, and <strong>📊 Dashboard</strong>. The active project and SOP follow you across all of them.</p>
|
||||
<p>From the home page, open <strong>SOP Configuration</strong>, the <strong>Work Package Creator</strong>, or the <strong>Dashboard</strong>. Inside the suite, switch any time using the top tabs: <strong>⚙SOP Configuration</strong>, <strong>Work Package Creation</strong>, and <strong>Dashboard</strong>. The active project and SOP follow you across all of them.</p>
|
||||
<h4>Quick start</h4>
|
||||
<ol>
|
||||
<li><strong>Open “SOP Configuration”</strong> and complete the 10 steps for your project (~15 minutes).</li>
|
||||
@@ -101,7 +101,7 @@
|
||||
<li><strong>Open “Work Package Creation”</strong> to author packages with your SOP defaults pre-populated.</li>
|
||||
<li><strong>Update from the field</strong> using the <strong>Field View</strong>, and <strong>leave feedback</strong> on any page with the Feedback button.</li>
|
||||
</ol>
|
||||
<div class="ui-help-callout">New here? On the home page choose the <strong>Sample Project</strong>, then click <strong>⭐ Load sample</strong> in the suite to see a fully filled-out SOP and an example Work Package.</div>` },
|
||||
<div class="ui-help-callout">New here? On the home page choose the <strong>Sample Project</strong>, then click <strong>Load sample data</strong> in the suite to see a fully filled-out SOP and an example Work Package.</div>` },
|
||||
|
||||
{ id: 'projects', title: 'Projects', body: `
|
||||
<h3>Projects</h3>
|
||||
@@ -133,7 +133,7 @@
|
||||
<li><strong>Release Gate Constraints</strong> — choose which standard AWP constraints apply and add custom ones (see <a data-help-jump="constraints">Constraints</a>).</li>
|
||||
<li><strong>Engineering Sources & References</strong> — labelled links (Design Drawings, Specs, …) that appear as quick-access buttons in the WP Creator's <em>Drawings & Attachments</em>.</li>
|
||||
</ol>
|
||||
<div class="ui-help-callout">Fields a WP inherits from the SOP show a <strong>"from SOP"</strong> tag and are locked. You can override a locked field with <strong>🔒 Edit</strong>, which requires a logged reason.</div>` },
|
||||
<div class="ui-help-callout">Fields a WP inherits from the SOP show a <strong>"from SOP"</strong> tag and are locked. You can override a locked field with <strong>✎ Edit</strong>, which requires a logged reason.</div>` },
|
||||
|
||||
{ id: 'wps', title: 'Work Packages', body: `
|
||||
<h3>Creating Work Packages</h3>
|
||||
@@ -152,7 +152,7 @@
|
||||
<li><strong>Quality / Hold Points</strong>, <strong>Approvals & Sign-offs</strong>, and <strong>Closeout</strong> (actual hours, as-builts, lessons learned — shown at QC/Closed).</li>
|
||||
</ul>
|
||||
<h4>Saving</h4>
|
||||
<p><strong>Save Draft</strong> stores the package; <strong>⚡ Save & View</strong> saves and renders the print-ready output. Drafts auto-save to your browser as you type, so nothing is lost if you close the tab.</p>` },
|
||||
<p><strong>Save Draft</strong> stores the package; <strong>Save & View</strong> saves and renders the print-ready output. Drafts auto-save to your browser as you type, so nothing is lost if you close the tab.</p>` },
|
||||
|
||||
{ id: 'statuses', title: 'Statuses', body: `
|
||||
<h3>Work Package statuses</h3>
|
||||
@@ -223,7 +223,7 @@
|
||||
|
||||
{ id: 'dashboard', title: 'Dashboard', body: `
|
||||
<h3>Dashboard & metrics</h3>
|
||||
<p>The dashboard aggregates every (non-master) package in the active project. Open it from the home page, the suite's <strong>📊 Dashboard</strong> tab, or the Creator header.</p>
|
||||
<p>The dashboard aggregates every (non-master) package in the active project. Open it from the home page, the suite's <strong>Dashboard</strong> tab, or the Creator header.</p>
|
||||
<h4>Metric cards (click to filter)</h4>
|
||||
<ul>
|
||||
<li><strong>Total WPs</strong>, <strong>Release-ready</strong>, <strong>On hold</strong>, <strong>Overdue</strong></li>
|
||||
@@ -232,7 +232,7 @@
|
||||
<h4>Breakdowns & gates</h4>
|
||||
<ul>
|
||||
<li><strong>By status</strong> and <strong>by discipline</strong> chips.</li>
|
||||
<li><strong>⛔ Gating constraints</strong> — lists every blocked package and exactly which constraints are holding it.</li>
|
||||
<li><strong>⊘ Gating constraints</strong> — lists every blocked package and exactly which constraints are holding it.</li>
|
||||
</ul>
|
||||
<h4>The table</h4>
|
||||
<p>Shows WP #, subject, type, discipline, status, <strong>Gates</strong> (<em>clear</em>, <em>n open</em>, or <em>master</em>), due date (red if overdue), and hours. Row actions: <strong>issue</strong> (when release-ready), <strong>view</strong>, and <strong>edit</strong>. Filter with the search box and the status / discipline dropdowns.</p>
|
||||
@@ -241,7 +241,7 @@
|
||||
{ id: 'data', title: 'Samples, sharing & comments', body: `
|
||||
<h3>Samples, import / export & comments</h3>
|
||||
<h4>Load sample</h4>
|
||||
<p><strong>⭐ Load sample</strong> is context-aware: on the SOP tab it loads a complete sample SOP; on the WP tab it loads an example Work Package. Great for learning the tool or demoing.</p>
|
||||
<p><strong>Load sample data</strong> is context-aware: on the SOP tab it loads a complete sample SOP; on the WP tab it loads an example Work Package. Great for learning the tool or demoing.</p>
|
||||
<h4>Import / Export</h4>
|
||||
<ul>
|
||||
<li><strong>Work Packages</strong> — <em>⤓ Export (JSON)</em> downloads all saved packages; import restores them.</li>
|
||||
@@ -249,9 +249,9 @@
|
||||
<li><strong>Materials</strong> — import a bill of materials from Excel/CSV, or download a template.</li>
|
||||
</ul>
|
||||
<h4>Comments & feedback</h4>
|
||||
<p>Leave feedback from the home page, per-step comments in the SOP tool (<strong>💬 Step Comments</strong>), or package comments in the Creator's <strong>💬 Comments</strong> drawer. Comments are saved and can be exported/imported as <code>.json</code> so reviewers can share them — and, when the API is reachable, they're collected centrally too.</p>
|
||||
<p>Leave feedback from the home page, per-step comments in the SOP tool (<strong>Step Comments</strong>), or package comments in the Creator's <strong>Comments</strong> drawer. Comments are saved and can be exported/imported as <code>.json</code> so reviewers can share them — and, when the API is reachable, they're collected centrally too.</p>
|
||||
<h4>Usage logs</h4>
|
||||
<p><strong>📊 Usage Logs</strong> / <strong>▤ Usage Data</strong> shows session and event counts and can export the full log. A <strong>dev-mode</strong> toggle pauses tracking during demos.</p>` },
|
||||
<p><strong>Usage Logs</strong> / <strong>▤ Usage Data</strong> shows session and event counts and can export the full log. A <strong>dev-mode</strong> toggle pauses tracking during demos.</p>` },
|
||||
|
||||
{ id: 'shortcuts', title: 'Tips & shortcuts', body: `
|
||||
<h3>Tips & keyboard shortcuts</h3>
|
||||
|
||||
@@ -34,7 +34,7 @@ async function boot(){
|
||||
_scope = (status === 200 && json) ? json : { can_manage_users:false, scope:'projects',
|
||||
grantable_roles:[], grantable_project_roles:[], managed_projects:[], project_roles:PROJECT_ROLES };
|
||||
if(status !== 200){
|
||||
banner('scope-banner','bad','❌ '+apiError(status, json, 'Could not work out what you may do here')+
|
||||
banner('scope-banner','bad','✕ '+apiError(status, json, 'Could not work out what you may do here')+
|
||||
' Showing the directory read-only.');
|
||||
} else {
|
||||
renderScope();
|
||||
@@ -85,7 +85,7 @@ async function loadUsers(){
|
||||
const wrap = document.getElementById('users-table');
|
||||
const { status, json } = await api('GET','/api/auth/users');
|
||||
if(status !== 200 || !Array.isArray(json)){
|
||||
banner('users-banner','bad','❌ '+apiError(status, json, 'Could not load the directory'));
|
||||
banner('users-banner','bad','✕ '+apiError(status, json, 'Could not load the directory'));
|
||||
wrap.innerHTML = ''; return;
|
||||
}
|
||||
banner('users-banner','', '');
|
||||
@@ -354,11 +354,11 @@ async function createUser(){
|
||||
role: val('nu-role'), project_role: val('nu-project-role'),
|
||||
});
|
||||
if(status === 200){
|
||||
say('var(--green)','✅ Created '+username+'.');
|
||||
say('var(--green)','✓ Created '+username+'.');
|
||||
['nu-username','nu-fullname','nu-email','nu-password'].forEach(id => document.getElementById(id).value = '');
|
||||
loadUsers();
|
||||
} else {
|
||||
say('var(--red)','❌ '+apiError(status, json, 'Could not create the account'));
|
||||
say('var(--red)','✕ '+apiError(status, json, 'Could not create the account'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -519,7 +519,7 @@ function lockQuality(id){
|
||||
el.classList.toggle('sop-inherited', fromSOP); // comment 8: blue-tinted SOP field
|
||||
el.classList.toggle('locked-field', false);
|
||||
const wrap=el.closest('.field'); const btn=wrap&&wrap.querySelector('.lock-edit'); const note=wrap&&wrap.querySelector('.override-note');
|
||||
if(btn) btn.textContent = fromSOP ? '🔒 Edit (reason required)' : '↺ Revert to SOP';
|
||||
if(btn) btn.textContent = fromSOP ? '✎ Edit (reason required)' : '↺ Revert to SOP';
|
||||
if(note) note.innerHTML = pkgOverrides[id] ? `Overridden: ${esc(pkgOverrides[id])}` : '';
|
||||
}
|
||||
function editQuality(id){
|
||||
@@ -570,7 +570,7 @@ function renderSopRefLinks(){
|
||||
const srcs=sopLinkedSources();
|
||||
if(!srcs.length){ box.innerHTML=''; return; }
|
||||
box.innerHTML=`<div class="ref-links-title">Reference folders (from SOP) — navigate to find & copy the specific file link:</div>`+
|
||||
`<div class="ref-links">`+srcs.map(s=>`<a href="${hrefAttr(s.link)}" target="_blank" rel="noopener" class="ref-link">📁 ${esc(s.label)} <span class="ref-sys">${esc(s.system||'')}</span></a>`).join('')+`</div>`;
|
||||
`<div class="ref-links">`+srcs.map(s=>`<a href="${hrefAttr(s.link)}" target="_blank" rel="noopener" class="ref-link">▸ ${esc(s.label)} <span class="ref-sys">${esc(s.system||'')}</span></a>`).join('')+`</div>`;
|
||||
}
|
||||
function renderSpecFolderLink(){
|
||||
const el=document.getElementById('spec-folder-link'); if(!el) return;
|
||||
@@ -802,7 +802,7 @@ function renderPredHint(){
|
||||
el.textContent = 'None — this package can be released as soon as its constraints are cleared.';
|
||||
el.style.color = '';
|
||||
} else if(blocking.length){
|
||||
el.innerHTML = '⛔ Waiting on ' + blocking.map(p => esc(p.number || p.id) + ' (' + esc(p.status) + ')').join(', ') +
|
||||
el.innerHTML = '⊘ Waiting on ' + blocking.map(p => esc(p.number || p.id) + ' (' + esc(p.status) + ')').join(', ') +
|
||||
'. Release is gated until they are Closed.';
|
||||
el.style.color = 'var(--red)';
|
||||
} else {
|
||||
@@ -1241,7 +1241,7 @@ function wpFilesMirror(){
|
||||
function wpFilesRender(){
|
||||
const box=document.getElementById('wp-file-list'); if(!box) return;
|
||||
box.innerHTML=pkgFiles.map(f=>`<div class="wp-file-item">
|
||||
<a href="/api/files/${encodeURIComponent(f.id)}" target="_blank" rel="noopener">${f.mime==='application/pdf'?'📄':'🖼'} ${esc(f.name||'drawing')}</a>
|
||||
<a href="/api/files/${encodeURIComponent(f.id)}" target="_blank" rel="noopener">${esc(f.name||'drawing')}</a>
|
||||
<span class="wf-size">${wpFileSize(f.size||0)}</span>
|
||||
<input type="text" value="${(f.description||'').replace(/"/g,'"')}" placeholder="focus area, e.g. Tray section, Level 3 east only"
|
||||
aria-label="Description of ${esc(f.name||'drawing')}" onchange="wpFileDescSave('${f.id}', this.value)">
|
||||
@@ -1310,7 +1310,7 @@ function renderSopFileFolders(){
|
||||
const srcs=sopLinkedSources();
|
||||
box.innerHTML = srcs.length
|
||||
? `<div class="field-hint">1) Open a folder, multi-select files in SharePoint, then use <b>Copy link</b>:</div>`+
|
||||
`<div class="ref-links">`+srcs.map(s=>`<a href="${hrefAttr(s.link)}" target="_blank" rel="noopener" class="ref-link">📁 ${esc(s.label)} <span class="ref-sys">${esc(s.system||'')}</span></a>`).join('')+`</div>`
|
||||
`<div class="ref-links">`+srcs.map(s=>`<a href="${hrefAttr(s.link)}" target="_blank" rel="noopener" class="ref-link">▸ ${esc(s.label)} <span class="ref-sys">${esc(s.system||'')}</span></a>`).join('')+`</div>`
|
||||
: `<div class="field-hint">No SOP folders defined — load or import an SOP first.</div>`;
|
||||
}
|
||||
function toggleSopFilePanel(){
|
||||
@@ -1417,7 +1417,7 @@ function readiness(){
|
||||
function updateReleaseBanner(){
|
||||
const b=document.getElementById('release-banner'); const r=readiness(); const st=getRadio('status');
|
||||
let cls, txt;
|
||||
if(st==='Issue'){ cls='rb-hold'; txt=`⛔ On hold — ${r.open} constraint${r.open===1?'':'s'} reopened. Resolve to resume.`; }
|
||||
if(st==='Issue'){ cls='rb-hold'; txt=`⊘ On hold — ${r.open} constraint${r.open===1?'':'s'} reopened. Resolve to resume.`; }
|
||||
else if(st==='Ready for QA'){
|
||||
// CR-014: the QA decision lives where the state is announced. Accept moves to
|
||||
// QC; reject returns to the crew and REQUIRES a comment (the modal enforces
|
||||
@@ -2790,7 +2790,7 @@ function renderWpNav(){
|
||||
const h = live[live.length - 1];
|
||||
const why = h ? (h.constraint ? h.constraint + ': ' : '') + (h.details || '')
|
||||
: 'no reason recorded — log it from the status control';
|
||||
holdLine = '<span class="wp-nav-hold">⛔ ' + (open ? open + ' open — ' : '')
|
||||
holdLine = '<span class="wp-nav-hold">⊘ ' + (open ? open + ' open — ' : '')
|
||||
+ esc(why) + '</span>';
|
||||
}
|
||||
return '<button type="button" class="wp-nav-item' + active + '" onclick="wpNavOpen(' + r.i + ')"' +
|
||||
@@ -3430,7 +3430,7 @@ function renderDashboard(){
|
||||
|
||||
// gating panel — what's blocking release, from the server
|
||||
const gated=m.gating||[];
|
||||
h+=`<div class="dash-panel"><div class="dash-panel-title">⛔ Gating constraints (${gated.length} package${gated.length===1?'':'s'} blocked)</div>`;
|
||||
h+=`<div class="dash-panel"><div class="dash-panel-title">⊘ Gating constraints (${gated.length} package${gated.length===1?'':'s'} blocked)</div>`;
|
||||
h+= gated.length ? `<table class="dash-table"><thead><tr><th>WP #</th><th>Subject</th><th>Blocked by</th></tr></thead><tbody>`+
|
||||
gated.map(g=>`<tr><td class="row-label">${esc(g.number||'—')}</td><td>${esc(g.subject||'')}</td>
|
||||
<td>${(g.blocked_by||[]).map(c=>esc(c.name)+(c.comment?` <span style="color:var(--text-dim)">(${esc(c.comment)})</span>`:'')).join('<br>')}</td></tr>`).join('')+
|
||||
|
||||
@@ -440,12 +440,12 @@
|
||||
<div class="sub-heading">Quality, Inspection & Hold Points</div>
|
||||
<div class="field-grid">
|
||||
<div class="field"><label>QC required</label><input type="text" id="wp_qc" placeholder="from SOP" readonly>
|
||||
<div class="lock-row"><button type="button" class="lock-edit" onclick="editQuality('wp_qc')">🔒 Edit (reason required)</button><span class="override-note"></span></div></div>
|
||||
<div class="lock-row"><button type="button" class="lock-edit" onclick="editQuality('wp_qc')">✎ Edit (reason required)</button><span class="override-note"></span></div></div>
|
||||
<div class="field"><label>Photo documentation</label><input type="text" id="wp_photo" placeholder="from SOP" readonly>
|
||||
<div class="lock-row"><button type="button" class="lock-edit" onclick="editQuality('wp_photo')">🔒 Edit (reason required)</button><span class="override-note"></span></div></div>
|
||||
<div class="lock-row"><button type="button" class="lock-edit" onclick="editQuality('wp_photo')">✎ Edit (reason required)</button><span class="override-note"></span></div></div>
|
||||
</div>
|
||||
<div class="field field-grid col1"><div class="field"><label>Witness / hold points</label><textarea id="wp_hold" rows="2" readonly placeholder="from SOP"></textarea>
|
||||
<div class="lock-row"><button type="button" class="lock-edit" onclick="editQuality('wp_hold')">🔒 Edit (reason required)</button><span class="override-note"></span></div>
|
||||
<div class="lock-row"><button type="button" class="lock-edit" onclick="editQuality('wp_hold')">✎ Edit (reason required)</button><span class="override-note"></span></div>
|
||||
<div class="field-hint">Inherited from the SOP. A <strong>Hold Point</strong> stops work until inspection sign-off; a <strong>Witness Point</strong> is offered for inspection but work may proceed if declined.</div></div></div>
|
||||
</div>
|
||||
|
||||
@@ -476,7 +476,7 @@
|
||||
<div class="nav-row"><button class="btn btn-ghost" onclick="newPackage()">↺ Clear</button>
|
||||
<div style="display:flex;gap:10px">
|
||||
<button class="btn btn-ghost" onclick="savePackage(false)">Save draft</button>
|
||||
<button class="btn btn-generate" onclick="savePackage(true)">⚡ Save & view</button>
|
||||
<button class="btn btn-generate" onclick="savePackage(true)">Save & view</button>
|
||||
</div></div>
|
||||
|
||||
<!-- DASHBOARD -->
|
||||
@@ -614,7 +614,7 @@
|
||||
<span class="sticky-status" id="sticky-status"></span>
|
||||
<div class="sticky-actions">
|
||||
<button class="btn btn-ghost" onclick="savePackage(false)">Save draft</button>
|
||||
<button class="btn btn-generate" onclick="savePackage(true)">⚡ Save & view</button>
|
||||
<button class="btn btn-generate" onclick="savePackage(true)">Save & view</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
{ section: 'People' },
|
||||
{ href: 'users.html', match: /(^|\/)users\.html$/, icon: '☺', label: 'User Directory',
|
||||
sub: 'Who\'s on the project' },
|
||||
{ href: 'admin.html', match: /(^|\/)admin\.html$/, icon: '⚡', label: 'Admin Console',
|
||||
{ href: 'admin.html', match: /(^|\/)admin\.html$/, icon: '⚙', label: 'Admin Console',
|
||||
sub: 'Settings & diagnostics',
|
||||
show: function () { return typeof window.wpIsAdmin === 'function' && window.wpIsAdmin(); } },
|
||||
// Account actions, inherited from the flat user menu that used to sit in the app
|
||||
@@ -51,7 +51,7 @@
|
||||
// drawer already had; these two were its only unique contents, so they moved here
|
||||
// rather than being lost with it. `action` items render as buttons, not links.
|
||||
{ section: 'Account' },
|
||||
{ action: 'wpPreferences', icon: '⌚', label: 'Language & time',
|
||||
{ action: 'wpPreferences', icon: '◷', label: 'Language & time',
|
||||
sub: 'Dates, numbers and time zone' },
|
||||
{ action: 'wpChangePassword', icon: '⚿', label: 'Password', sub: 'Change your password' },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user