Tighten Importarr UI #30

This commit is contained in:
2026-07-29 20:28:09 +02:00
parent 2a7c39bf6a
commit da4df50205
3 changed files with 14 additions and 6 deletions
+2 -2
View File
@@ -1,2 +1,2 @@
body{font-family:system-ui,sans-serif;margin:0;background:#0a0a0a;color:#e5e7eb}header,main{max-width:1280px;margin:auto;box-sizing:border-box}.topbar{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:1rem;align-items:center;background:#050505;border-bottom:1px solid #3f3f46;padding:.55rem 1rem;position:sticky;top:0;z-index:2}.brand h1{font-size:1.5rem;margin:0;color:#facc15}.brand p,.top-status span{margin:0;color:#a3a3a3}.top-status{min-width:0;text-align:center}.top-status strong{font-size:1rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.top-controls{display:flex;align-items:center;gap:.35rem}.top-controls button,.menu summary{min-width:2.6rem;min-height:2.4rem;padding:.45rem;border-radius:0;background:#e5e7eb;color:#111827;border:1px solid #52525b;text-align:center}.menu{position:relative}.menu summary{list-style:none;cursor:pointer;font-weight:800;font-size:1.35rem}.menu summary::-webkit-details-marker{display:none}.menu-panel{position:absolute;right:0;top:calc(100% + .4rem);display:grid;gap:.4rem;min-width:14rem;background:#27272a;border:1px solid #52525b;padding:.6rem;box-shadow:0 .7rem 2rem #0009}.menu-panel a,.menu-panel button{display:block;width:100%;box-sizing:border-box;text-align:left;color:#e5e7eb;background:#3f3f46;text-decoration:none;border:1px solid #52525b;padding:.65rem;border-radius:.25rem;font-weight:700}.menu-panel .danger{background:#7f1d1d;color:#fee2e2}main{padding:0 1rem 1rem}.summary-strip{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:1px;background:#3f3f46;margin-top:1rem;border:1px solid #3f3f46}.summary-strip article,.panel{background:#525252}.summary-strip article{padding:.75rem;min-width:0}.summary-strip strong{display:block;font-size:1.1rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.summary-strip span,small,dd{color:#d4d4d4}.panel{padding:1rem;margin-top:1rem;border:1px solid #3f3f46}.packed{background:#262626}.packed summary{cursor:pointer;font-weight:800;font-size:1.1rem}.queue-panel{background:#737373;color:#f5f5f5}.section-title{display:flex;justify-content:space-between;gap:1rem;align-items:center}.section-title h2,.job-group h3{font-size:2rem;line-height:1;margin:.25rem 0 1rem}.section-title span,.job-group h3 span{font-size:1rem;color:#e5e5e5}table{width:100%;border-collapse:collapse;background:#3f3f46;margin-top:.5rem}.table-scroll{max-width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-scroll table{min-width:42rem}.jobs-table table{table-layout:fixed;min-width:0}.jobs-table th:nth-child(1){width:52%}.jobs-table th:nth-child(2){width:20%}.jobs-table th:nth-child(3){width:11%}.jobs-table th:nth-child(4){width:7%}.jobs-table th:nth-child(5){width:10%}.jobs-table td{overflow:hidden;text-overflow:ellipsis}.jobs-table .file-name{font-size:1rem;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.jobs-table .row-actions{min-width:5.5rem}.jobs-table .row-actions button{width:100%}th,td{padding:.55rem;border-bottom:1px solid #27272a;text-align:left;vertical-align:middle}input,button{padding:.6rem;border-radius:.25rem;border:1px solid #52525b;box-sizing:border-box}button{background:#d4d4d8;color:#18181b;font-weight:700;cursor:pointer;min-height:2.5rem}.danger{background:#f87171;color:#450a0a}.warn{background:#fbbf24;color:#451a03}.controls,.row-actions{display:flex;gap:.4rem;flex-wrap:wrap}.inline-form{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.75rem}.inline-form input[name=path]{min-width:min(100%,28rem);flex:1}.info{display:grid;grid-template-columns:10rem 1fr;gap:.4rem 1rem}.info dt{font-weight:700}.info dd{margin:0;overflow-wrap:anywhere}.state{background:#27272a;border:1px solid #52525b;border-radius:999px;padding:.15rem .5rem;display:inline-block;color:#f5f5f5}.job-group{margin-top:1rem}.job-group h3{display:flex;justify-content:space-between}.job-cards{display:none}.job-card{background:#3f3f46;border:1px solid #27272a;margin:.5rem 0;padding:.75rem}.job-card .file-name{font-size:1.05rem;word-break:break-word}.job-card dl{display:grid;grid-template-columns:6rem 1fr;gap:.35rem;margin:.7rem 0}.job-card dt{font-weight:700}.job-card dd{margin:0;word-break:break-word}dialog{max-width:min(48rem,95vw);background:#1f2937;color:#e5e7eb;border:1px solid #374151;border-radius:.75rem}dialog::backdrop{background:#0009}fieldset{border:1px solid #374151;margin:1rem 0}label{display:grid;gap:.35rem;margin:.75rem 0}.success{color:#86efac}.error{color:#fca5a5}.hint{color:#9ca3af} body{font-family:system-ui,sans-serif;margin:0;background:#111225;color:#eef2ff;font-size:14px}header,main{max-width:1280px;margin:auto;box-sizing:border-box}.topbar{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:.8rem;align-items:center;background:#17123b;border-bottom:1px solid #4f46e5;padding:.45rem .9rem;position:sticky;top:0;z-index:2}.brand h1{font-size:1.35rem;margin:0;color:#93c5fd}.brand p,.top-status span{margin:0;color:#c4b5fd}.top-status{min-width:0;text-align:center}.top-status strong{font-size:.92rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.top-controls{display:flex;align-items:center;gap:.3rem}.top-controls button,.menu summary{min-width:2.25rem;min-height:2.15rem;padding:.35rem;border-radius:.3rem;background:#bfdbfe;color:#1e1b4b;border:1px solid #818cf8;text-align:center}.menu{position:relative}.menu summary{list-style:none;cursor:pointer;font-weight:800;font-size:1.15rem}.menu summary::-webkit-details-marker{display:none}.menu-panel{position:absolute;right:0;top:calc(100% + .35rem);display:grid;gap:.35rem;min-width:13rem;background:#21194f;border:1px solid #818cf8;padding:.5rem;box-shadow:0 .7rem 2rem #0009}.menu-panel a,.menu-panel button{display:block;width:100%;box-sizing:border-box;text-align:left;color:#eef2ff;background:#312e81;text-decoration:none;border:1px solid #6366f1;padding:.5rem;border-radius:.25rem;font-weight:700}.menu-panel .danger{background:#7f1d1d;color:#fee2e2}main{padding:0 .9rem .9rem}.summary-strip{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:1px;background:#4338ca;margin-top:.8rem;border:1px solid #4338ca}.summary-strip article,.panel{background:#2e2a6f}.summary-strip article{padding:.55rem;min-width:0}.summary-strip strong{display:block;font-size:.95rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.summary-strip span,small,dd{color:#c7d2fe}.panel{padding:.8rem;margin-top:.8rem;border:1px solid #4338ca}.packed{background:#1e1b4b}.packed summary{cursor:pointer;font-weight:800;font-size:1rem}.queue-panel{background:#25245c;color:#eef2ff}.section-title{display:flex;justify-content:space-between;gap:1rem;align-items:center}.section-title h2,.job-group h3{font-size:1.55rem;line-height:1;margin:.2rem 0 .65rem;color:#dbeafe}.section-title span,.job-group h3 span{font-size:.85rem;color:#c4b5fd}table{width:100%;border-collapse:collapse;background:#1e1b4b;margin-top:.35rem}.table-scroll{max-width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-scroll table{min-width:42rem}.jobs-table table{table-layout:fixed;min-width:0}.jobs-table th:nth-child(1){width:66%}.jobs-table th:nth-child(2){width:12%}.jobs-table th:nth-child(3){width:10%}.jobs-table th:nth-child(4){width:12%}.jobs-table td{overflow:hidden;text-overflow:ellipsis}.jobs-table .file-name{font-size:.9rem;line-height:1.15;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.jobs-table small{display:block;font-size:.76rem;line-height:1.15;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}th,td{padding:.36rem .45rem;border-bottom:1px solid #3730a3;text-align:left;vertical-align:middle}th{font-size:.75rem;color:#bfdbfe;font-weight:700}input,button{padding:.48rem;border-radius:.25rem;border:1px solid #6366f1;box-sizing:border-box}button{background:#bfdbfe;color:#1e1b4b;font-weight:700;cursor:pointer;min-height:2.15rem}.danger{background:#f87171;color:#450a0a}.warn{background:#fde68a;color:#451a03}.controls,.row-actions{display:flex;gap:.25rem;flex-wrap:nowrap}.row-actions button{width:1.85rem;min-width:1.85rem;min-height:1.85rem;padding:.2rem;font-size:.82rem;line-height:1}.inline-form{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.6rem}.inline-form input[name=path]{min-width:min(100%,28rem);flex:1}.info{display:grid;grid-template-columns:10rem 1fr;gap:.35rem .8rem}.info dt{font-weight:700}.info dd{margin:0;overflow-wrap:anywhere}.state{background:#312e81;border:1px solid #93c5fd;border-radius:999px;padding:.08rem .42rem;display:inline-block;color:#dbeafe;font-size:.75rem;font-weight:800;line-height:1.4}.job-group{margin-top:.8rem}.job-group h3{display:flex;justify-content:space-between}.job-cards{display:none}.job-card{background:#1e1b4b;border:1px solid #3730a3;margin:.4rem 0;padding:.58rem}.job-card .file-name{font-size:.95rem;word-break:break-word}.job-card dl{display:grid;grid-template-columns:5rem 1fr;gap:.25rem;margin:.45rem 0}.job-card dt{font-weight:700}.job-card dd{margin:0;word-break:break-word}dialog{max-width:min(48rem,95vw);background:#1e1b4b;color:#eef2ff;border:1px solid #818cf8;border-radius:.6rem}dialog::backdrop{background:#0009}fieldset{border:1px solid #4338ca;margin:.8rem 0}label{display:grid;gap:.3rem;margin:.6rem 0}.success{color:#86efac}.error{color:#fca5a5}.hint{color:#c4b5fd}
@media (max-width:640px){header,main{max-width:1280px}.topbar{grid-template-columns:auto 1fr auto;padding:.45rem .65rem}.brand h1{font-size:1.15rem}.brand p{font-size:.8rem}.top-status{text-align:left}.top-status span{display:none}.top-status strong{font-size:.95rem}.summary-strip{grid-template-columns:1fr 1fr}.summary-strip article{padding:.6rem}.panel{padding:.75rem}.section-title h2,.job-group h3{font-size:1.75rem}.jobs-table{display:none}.job-cards{display:block}.table-scroll{overflow-x:auto}.controls,.inline-form{flex-direction:column}.controls button,.inline-form button,.inline-form input{width:100%}.info{grid-template-columns:1fr}.menu-panel{right:0;min-width:12rem}.row-actions button{flex:1 1 7rem}.job-card .file-name{word-break:break-word}} @media (max-width:640px){header,main{max-width:1280px}.topbar{grid-template-columns:auto 1fr auto;padding:.4rem .55rem}.brand h1{font-size:1.05rem}.brand p{font-size:.75rem}.top-status{text-align:left}.top-status span{display:none}.top-status strong{font-size:.85rem}.summary-strip{grid-template-columns:1fr 1fr}.summary-strip article{padding:.5rem}.panel{padding:.65rem}.section-title h2,.job-group h3{font-size:1.35rem}.jobs-table{display:none}.job-cards{display:block}.table-scroll{overflow-x:auto}.controls,.inline-form{flex-direction:column}.controls button,.inline-form button,.inline-form input{width:100%}.info{grid-template-columns:1fr}.menu-panel{right:0;min-width:12rem}.job-card .file-name{word-break:break-word}.row-actions{justify-content:flex-end}.row-actions button{flex:0 0 1.9rem}}
+6 -2
View File
@@ -109,8 +109,10 @@
<script> <script>
const esc=value=>String(value??'').replace(/[&<>"']/g,ch=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch])); const esc=value=>String(value??'').replace(/[&<>"']/g,ch=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]));
async function postJson(url, body){ const response=await fetch(url,{method:'POST',headers:{'content-type':'application/json'},body:body?JSON.stringify(body):undefined}); if(!response.ok){ const error=await response.json().catch(()=>({detail:response.statusText})); alert(error.detail||'Request failed'); } return response; } async function postJson(url, body){ const response=await fetch(url,{method:'POST',headers:{'content-type':'application/json'},body:body?JSON.stringify(body):undefined}); if(!response.ok){ const error=await response.json().catch(()=>({detail:response.statusText})); alert(error.detail||'Request failed'); } return response; }
function actionButtons(j){ const buttons=[]; if(j.can_run_now) buttons.push(`<button type="button" data-action="run-now" data-id="${j.id}">Run now</button>`); if(j.can_retry) buttons.push(`<button type="button" data-action="retry" data-id="${j.id}">Retry</button>`); if(j.can_ignore) buttons.push(`<button type="button" data-action="ignore" data-id="${j.id}" class="warn">Ignore</button>`); if(j.can_remove) buttons.push(`<button type="button" data-action="remove" data-id="${j.id}" class="danger">Remove</button>`); return buttons.join(' '); } function actionButtons(j){ const buttons=[]; if(j.can_run_now) buttons.push(`<button type="button" data-action="run-now" data-id="${j.id}" title="Run now">▶</button>`); if(j.can_retry) buttons.push(`<button type="button" data-action="retry" data-id="${j.id}" title="Retry">↻</button>`); if(j.can_ignore) buttons.push(`<button type="button" data-action="ignore" data-id="${j.id}" class="warn" title="Ignore">!</button>`); if(j.can_remove) buttons.push(`<button type="button" data-action="remove" data-id="${j.id}" class="danger" title="Remove">🗑</button>`); return buttons.join(' '); }
function renderJobs(data){ const groups=(data.groups||[]).filter(group=>group.jobs.length); if(!groups.length) return '<p>No queue items.</p>'; return groups.map(group=>`<section class="job-group"><h3>${esc(group.label)} <span>${group.jobs.length}</span></h3><div class="table-scroll jobs-table"><table><thead><tr><th>File</th><th>Context</th><th>Readiness</th><th>SAB</th><th>Actions</th></tr></thead><tbody>${group.jobs.map(j=>`<tr><td><strong class="file-name">${esc(j.name)}</strong><small>${esc(j.source_type)}${j.batch_id?' · batch '+esc(j.batch_id):''}</small></td><td><small>${esc(j.relative_path||j.storage||j.source_id)}</small></td><td><span class="state">${esc(j.state)}</span><small>${esc(j.reason||'')}</small></td><td><small>${esc(j.sab_status||'—')}${j.sab_category?' · '+esc(j.sab_category):''}</small></td><td class="row-actions">${actionButtons(j)}</td></tr>`).join('')}</tbody></table></div><div class="job-cards">${group.jobs.map(j=>`<article class="job-card"><strong class="file-name">${esc(j.name)}</strong><small>${esc(j.source_type)}${j.batch_id?' · batch '+esc(j.batch_id):''}</small><dl><dt>Context</dt><dd>${esc(j.relative_path||j.storage||j.source_id)}</dd><dt>Readiness</dt><dd><span class="state">${esc(j.state)}</span>${j.reason?`<small>${esc(j.reason)}</small>`:''}</dd><dt>SAB</dt><dd>${esc(j.sab_status||'—')}${j.sab_category?' · '+esc(j.sab_category):''}</dd></dl><div class="row-actions">${actionButtons(j)}</div></article>`).join('')}</div></section>`).join(''); } function jobSubtext(j){ return `${esc(j.source_type)}${j.batch_id?' · batch '+esc(j.batch_id):''} · ${esc(j.relative_path||j.storage||j.source_id)}`; }
function readiness(j){ return `<span class="state" title="${esc(j.reason||j.state)}">${esc(j.state)}</span>`; }
function renderJobs(data){ const groups=(data.groups||[]).filter(group=>group.jobs.length); if(!groups.length) return '<p>No queue items.</p>'; return groups.map(group=>`<section class="job-group"><h3>${esc(group.label)} <span>${group.jobs.length}</span></h3><div class="table-scroll jobs-table"><table><thead><tr><th>File</th><th>Readiness</th><th>SAB</th><th>Actions</th></tr></thead><tbody>${group.jobs.map(j=>`<tr><td><strong class="file-name">${esc(j.name)}</strong><small>${jobSubtext(j)}</small></td><td>${readiness(j)}</td><td><small>${esc(j.sab_status||'—')}${j.sab_category?' · '+esc(j.sab_category):''}</small></td><td class="row-actions">${actionButtons(j)}</td></tr>`).join('')}</tbody></table></div><div class="job-cards">${group.jobs.map(j=>`<article class="job-card"><strong class="file-name">${esc(j.name)}</strong><small>${jobSubtext(j)}</small><dl><dt>Readiness</dt><dd>${readiness(j)}</dd><dt>SAB</dt><dd>${esc(j.sab_status||'—')}${j.sab_category?' · '+esc(j.sab_category):''}</dd></dl><div class="row-actions">${actionButtons(j)}</div></article>`).join('')}</div></section>`).join(''); }
async function refresh(){ const r=await fetch('/api/jobs'); const d=await r.json(); document.title=d.jobs.length?`📥 ${d.jobs.length} jobs - Importarr`:'📥 - idle · Importarr'; document.getElementById('jobs').innerHTML=renderJobs(d); if(d.control){ const current=d.control.current||'idle'; document.getElementById('queue-mode').textContent=d.control.queue_mode; document.getElementById('current-job').textContent=current; document.getElementById('top-current-job').textContent=current; document.getElementById('ready-state').textContent=d.control.queue_mode==='start'?'Running':d.control.queue_mode; } } async function refresh(){ const r=await fetch('/api/jobs'); const d=await r.json(); document.title=d.jobs.length?`📥 ${d.jobs.length} jobs - Importarr`:'📥 - idle · Importarr'; document.getElementById('jobs').innerHTML=renderJobs(d); if(d.control){ const current=d.control.current||'idle'; document.getElementById('queue-mode').textContent=d.control.queue_mode; document.getElementById('current-job').textContent=current; document.getElementById('top-current-job').textContent=current; document.getElementById('ready-state').textContent=d.control.queue_mode==='start'?'Running':d.control.queue_mode; } }
document.getElementById('jobs').addEventListener('click', async e=>{ const button=e.target.closest('button[data-action]'); if(!button)return; const action=button.dataset.action; const destructive=['ignore','remove'].includes(action); if(destructive&&!confirm(`${action} this Importarr queue item?`)) return; await postJson(`/api/queue-items/${button.dataset.id}/action`,{action}); await refresh(); }); document.getElementById('jobs').addEventListener('click', async e=>{ const button=e.target.closest('button[data-action]'); if(!button)return; const action=button.dataset.action; const destructive=['ignore','remove'].includes(action); if(destructive&&!confirm(`${action} this Importarr queue item?`)) return; await postJson(`/api/queue-items/${button.dataset.id}/action`,{action}); await refresh(); });
document.querySelectorAll('[data-control]').forEach(button=>button.addEventListener('click', async()=>{ if(button.dataset.control==='cancel-current'&&!confirm('Cancel the current import job?')) return; await postJson(`/api/control/${button.dataset.control}`); await refresh(); })); document.querySelectorAll('[data-control]').forEach(button=>button.addEventListener('click', async()=>{ if(button.dataset.control==='cancel-current'&&!confirm('Cancel the current import job?')) return; await postJson(`/api/control/${button.dataset.control}`); await refresh(); }));
@@ -119,6 +121,8 @@
document.getElementById('batch-form').addEventListener('submit', async e=>{ e.preventDefault(); const response=await postJson('/api/manual-batches',{path:e.target.path.value}); if(response.ok) location.reload(); }); document.getElementById('batch-form').addEventListener('submit', async e=>{ e.preventDefault(); const response=await postJson('/api/manual-batches',{path:e.target.path.value}); if(response.ok) location.reload(); });
document.getElementById('open-settings').addEventListener('click',()=>document.getElementById('settings-dialog').showModal()); document.getElementById('open-settings').addEventListener('click',()=>document.getElementById('settings-dialog').showModal());
document.getElementById('close-settings').addEventListener('click',()=>document.getElementById('settings-dialog').close()); document.getElementById('close-settings').addEventListener('click',()=>document.getElementById('settings-dialog').close());
document.addEventListener('click',e=>{ document.querySelectorAll('details.menu[open]').forEach(menu=>{ if(!menu.contains(e.target)) menu.removeAttribute('open'); }); });
document.getElementById('settings-dialog').addEventListener('click',e=>{ if(e.target===e.currentTarget) e.currentTarget.close(); });
document.getElementById('settings-form').addEventListener('submit', async e=>{ e.preventDefault(); const body=Object.fromEntries(new FormData(e.target)); const response=await postJson('/api/settings',body); if(response.ok){ const data=await response.json(); document.getElementById('sab-token-status').textContent=data.sab_api_key_configured?'configured':'not configured'; ['sab_api_key','radarr_api_key','sonarr_api_key'].forEach(name=>e.target.elements[name].value=''); document.getElementById('settings-dialog').close(); } }); document.getElementById('settings-form').addEventListener('submit', async e=>{ e.preventDefault(); const body=Object.fromEntries(new FormData(e.target)); const response=await postJson('/api/settings',body); if(response.ok){ const data=await response.json(); document.getElementById('sab-token-status').textContent=data.sab_api_key_configured?'configured':'not configured'; ['sab_api_key','radarr_api_key','sonarr_api_key'].forEach(name=>e.target.elements[name].value=''); document.getElementById('settings-dialog').close(); } });
document.querySelectorAll('[data-test-connection]').forEach(button=>button.addEventListener('click', async()=>{ const form=document.getElementById('settings-form'); const service=button.dataset.testConnection; const prefix=service==='sabnzbd'?'sab':service; const output=document.getElementById(`${service}-test-result`); output.textContent='Testing…'; output.className=''; const response=await postJson('/api/settings/test-connection',{service,url:form.elements[`${prefix}_url`].value,api_key:form.elements[`${prefix}_api_key`].value}); if(response.ok){ const data=await response.json(); output.textContent=data.message; output.className=data.ok?'success':'error'; } })); document.querySelectorAll('[data-test-connection]').forEach(button=>button.addEventListener('click', async()=>{ const form=document.getElementById('settings-form'); const service=button.dataset.testConnection; const prefix=service==='sabnzbd'?'sab':service; const output=document.getElementById(`${service}-test-result`); output.textContent='Testing…'; output.className=''; const response=await postJson('/api/settings/test-connection',{service,url:form.elements[`${prefix}_url`].value,api_key:form.elements[`${prefix}_api_key`].value}); if(response.ok){ const data=await response.json(); output.textContent=data.message; output.className=data.ok?'success':'error'; } }));
document.getElementById('force-run').addEventListener('click', async()=>{ await postJson('/api/import/run-now',{force:true}); await refresh(); }); document.getElementById('force-run').addEventListener('click', async()=>{ await postJson('/api/import/run-now',{force:true}); await refresh(); });
+6 -2
View File
@@ -59,6 +59,10 @@ def test_index_packs_secondary_controls_into_menu(tmp_path, monkeypatch):
assert "Service info and build details" in response.text assert "Service info and build details" in response.text
assert "Queue and history" in response.text assert "Queue and history" in response.text
assert "Current import" in response.text assert "Current import" in response.text
assert "details.menu[open]" in response.text
assert "e.target===e.currentTarget" in response.text
assert "function readiness" in response.text
assert 'title="${esc(j.reason||j.state)}"' in response.text
def test_index_renders_settings_dialog(tmp_path, monkeypatch): def test_index_renders_settings_dialog(tmp_path, monkeypatch):
@@ -108,8 +112,8 @@ def test_stylesheet_includes_mobile_responsive_rules():
assert "word-break:break-word" in css assert "word-break:break-word" in css
assert "max-width:1280px" in css assert "max-width:1280px" in css
assert ".jobs-table table{table-layout:fixed;min-width:0}" in css assert ".jobs-table table{table-layout:fixed;min-width:0}" in css
assert ".jobs-table th:nth-child(1){width:52%}" in css assert ".jobs-table th:nth-child(1){width:66%}" in css
assert ".jobs-table .row-actions button{width:100%}" in css assert ".row-actions button{width:1.85rem" in css
assert ".summary-strip" in css assert ".summary-strip" in css
assert ".menu-panel" in css assert ".menu-panel" in css