dataforth/testdatadb UI: clear, persistent push feedback (toasts)

The push handlers set the button to 'skipped' then immediately ran search(), which
re-rendered the inspector and wiped the text — so a skipped publish flashed and
vanished (looked like nothing happened). Replace with persistent toasts that state the
outcome explicitly: Published / already up-to-date / Push failed / and for a skip,
'<model> isn't renderable yet, so nothing was sent.' Only refresh the row on an actual
publish so the message isn't clobbered. Same for the multi-select Re-push summary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 16:04:31 -07:00
parent 0d22704f65
commit 1c9f2d101d

View File

@@ -150,6 +150,12 @@
.viewer iframe{display:block;width:100%;min-height:100%;border:1px solid var(--border);border-radius:5px;background:#fff; .viewer iframe{display:block;width:100%;min-height:100%;border:1px solid var(--border);border-radius:5px;background:#fff;
box-shadow:0 1px 5px rgba(15,23,42,.10)} box-shadow:0 1px 5px rgba(15,23,42,.10)}
kbd{font-family:var(--mono);font-size:11px;background:#f1f5f9;border:1px solid var(--border-strong);border-bottom-width:2px;border-radius:4px;padding:0 5px;color:var(--ink-2)} kbd{font-family:var(--mono);font-size:11px;background:#f1f5f9;border:1px solid var(--border-strong);border-bottom-width:2px;border-radius:4px;padding:0 5px;color:var(--ink-2)}
/* ---------- toast ---------- */
.toast{position:fixed;right:18px;bottom:18px;max-width:400px;z-index:60;display:flex;flex-direction:column;gap:8px}
.toast .t{background:#fff;border:1px solid var(--border);border-left:4px solid var(--accent);border-radius:8px;box-shadow:0 8px 28px rgba(15,23,42,.18);padding:11px 13px 12px;font-size:12.5px;color:var(--ink-2);cursor:pointer;animation:tin .16s}
.toast .t.ok{border-left-color:var(--pass-ink)} .toast .t.warn{border-left-color:#d97706} .toast .t.err{border-left-color:var(--fail-ink)}
.toast .t b{display:block;margin-bottom:2px;color:var(--ink);font-size:13px}
@keyframes tin{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
/* ---------- responsive ---------- */ /* ---------- responsive ---------- */
@media (max-width:1180px){ @media (max-width:1180px){
main{grid-template-columns:1fr 0} main{grid-template-columns:1fr 0}
@@ -242,6 +248,7 @@
<div class="viewer" id="viewer"></div> <div class="viewer" id="viewer"></div>
</aside> </aside>
</main> </main>
<div class="toast" id="toast"></div>
<script> <script>
const API=''; const API='';
@@ -381,23 +388,31 @@ async function doUpload(payload){
const r=await fetch(API+'/api/upload',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}); const r=await fetch(API+'/api/upload',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const d=await r.json().catch(()=>({})); if(!r.ok) throw new Error(d.error||('HTTP '+r.status)); return d; const d=await r.json().catch(()=>({})); if(!r.ok) throw new Error(d.error||('HTTP '+r.status)); return d;
} }
function showToast(title,msg,type){ const c=$('toast'); if(!c)return; const el=document.createElement('div');
el.className='t '+(type||''); el.innerHTML='<b>'+esc(title)+'</b>'+(msg?esc(msg):''); el.onclick=()=>el.remove();
c.appendChild(el); setTimeout(()=>{el.style.transition='opacity .3s';el.style.opacity='0';setTimeout(()=>el.remove(),320);}, type==='warn'||type==='err'?9000:5000); }
async function pushWeb(id,btn){ async function pushWeb(id,btn){
const r=state.rows.find(x=>x.id==id); const sn=r?r.serial_number:id; const r=state.rows.find(x=>x.id==id); const sn=r?r.serial_number:id; const mdl=r?r.model_number:'';
if(!confirm('Publish '+sn+' to the public Dataforth website now?')) return; if(!confirm('Publish '+sn+' to the public Dataforth website now?')) return;
const t=btn.textContent; btn.disabled=true; btn.textContent='Publishing…'; const t=btn.textContent; btn.disabled=true; btn.textContent='Publishing…';
try{ const d=await doUpload({ids:[+id]}); try{ const d=await doUpload({ids:[+id]}); const pub=(d.created||0)+(d.updated||0);
btn.textContent=d.errors?('✕ '+d.errors+' err'):(d.skipped&&!(d.created+d.updated+d.unchanged)?'skipped':'Published ✓'); if(pub>0){ showToast('Published — '+sn,'Sent to the public website.','ok'); setTimeout(search,400); }
setTimeout(search,500); // refresh WEB status from the DB else if(d.errors){ showToast('Push failed — '+sn, d.errors+' error(s) from the website API.','err'); }
}catch(e){ btn.textContent='✕ '+e.message.slice(0,16); btn.disabled=false; } else if(d.skipped){ showToast('Not published — '+sn, mdl+' isnt renderable yet, so nothing was sent. This model still needs the render fix before it can publish.','warn'); }
else if(d.unchanged){ showToast(sn+' already up to date','Already on the website — nothing changed.','ok'); }
else { showToast('No change — '+sn, 'Nothing was published.','warn'); }
}catch(e){ showToast('Push failed — '+sn, e.message,'err'); }
btn.disabled=false; btn.textContent=t;
} }
async function pushSelected(){ async function pushSelected(){
const ids=[...state.checks].map(Number); if(!ids.length) return; const ids=[...state.checks].map(Number); if(!ids.length) return;
if(!confirm('Publish '+ids.length+' selected serial(s) to the public Dataforth website now?')) return; if(!confirm('Publish '+ids.length+' selected serial(s) to the public Dataforth website now?')) return;
const b=$('repushSel'),t=b.textContent; b.disabled=true; b.textContent='Publishing…'; const b=$('repushSel'),t=b.textContent; b.disabled=true; b.textContent='Publishing…';
try{ const d=await doUpload({ids}); try{ const d=await doUpload({ids}); const pub=(d.created||0)+(d.updated||0);
b.textContent=' '+((d.created||0)+(d.updated||0))+' pushed'+(d.skipped?(' · '+d.skipped+' skip'):''); showToast('Push complete', pub+' published · '+(d.unchanged||0)+' unchanged · '+(d.skipped||0)+' skipped (not renderable)'+(d.errors?(' · '+d.errors+' error'):'')+'.', d.errors?'err':(pub||d.unchanged?'ok':'warn'));
setTimeout(()=>{b.textContent=t;b.disabled=false;search();},1400); if(pub) search();
}catch(e){ b.textContent='✕ failed'; b.disabled=false; alert('Push failed: '+e.message); } }catch(e){ showToast('Push failed', e.message,'err'); }
b.disabled=false; b.textContent=t;
} }
$('repushSel').onclick=pushSelected; $('repushSel').onclick=pushSelected;