Make UI SABnzbd-style #30
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
body{font-family:system-ui,sans-serif;margin:0;background:#111827;color:#e5e7eb}header,main{max-width:1280px;margin:auto;padding:1rem;box-sizing:border-box}.topbar{display:flex;justify-content:space-between;gap:1rem;align-items:center;background:#0f172a}.build{text-align:right}.build strong{font-size:1.2rem}.build span{display:block;overflow-wrap:anywhere}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(12rem,100%),1fr));gap:1rem}.cards article,.panel{background:#1f2937;border-radius:.75rem;padding:1rem;margin-top:1rem}strong{display:block;font-size:2rem}span,small,dd{color:#9ca3af}table{width:100%;border-collapse:collapse;background:#1f2937;margin-top:1rem}.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:48%}.jobs-table th:nth-child(2){width:20%}.jobs-table th:nth-child(3){width:10%}.jobs-table th:nth-child(4){width:7%}.jobs-table th:nth-child(5){width:15%}.jobs-table td{overflow-wrap:anywhere}.jobs-table .file-name{font-size:1.05rem;line-height:1.3}.jobs-table .row-actions{min-width:5.5rem}.jobs-table .row-actions button{width:100%}th,td{padding:.6rem;border-bottom:1px solid #374151;text-align:left;vertical-align:top}input,button{padding:.6rem;border-radius:.4rem;border:1px solid #374151;box-sizing:border-box}button{background:#38bdf8;color:#082f49;font-weight:700;cursor:pointer;min-height:2.75rem}.danger{background:#f87171;color:#450a0a}.warn{background:#fbbf24;color:#451a03}.controls,.row-actions{display:flex;gap:.5rem;flex-wrap:wrap}.inline-form{display:flex;gap:.5rem;flex-wrap:wrap}.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:#0f172a;border:1px solid #374151;border-radius:999px;padding:.15rem .5rem;display:inline-block}.section-title{display:flex;align-items:center;justify-content:space-between;gap:1rem}.job-group{margin-top:1.25rem}.job-group h3{display:flex;gap:.5rem;align-items:center}.job-group h3 span{font-size:.9rem;border:1px solid #374151;border-radius:999px;padding:.1rem .45rem}.job-cards{display:none}.job-card{background:#111827;border:1px solid #374151;border-radius:.75rem;margin-top:.75rem;padding:.85rem}.job-card dl{display:grid;gap:.35rem;margin:.75rem 0}.job-card dt{font-weight:700}.job-card dd{margin:0;overflow-wrap:anywhere}.file-name{font-size:1rem}.row-actions button{padding:.35rem .5rem}td small{display:block;overflow-wrap:anywhere}dialog{background:#1f2937;color:#e5e7eb;border:1px solid #374151;border-radius:.75rem;max-width:min(42rem,90vw);box-sizing:border-box}dialog::backdrop{background:#0009}fieldset{border:1px solid #374151;border-radius:.5rem;margin:1rem 0;padding:1rem}label{display:grid;gap:.35rem;margin:.75rem 0}.hint{color:#9ca3af}output{display:block;margin-top:.5rem;color:#9ca3af}.success{color:#86efac}.error{color:#fca5a5}@media (max-width:640px){header,main{padding:.75rem}.topbar{align-items:flex-start;flex-direction:column}.topbar h1{margin-bottom:.25rem}.build{text-align:left;width:100%}.build button{margin-top:.5rem;width:100%}.cards{gap:.75rem}.cards article,.panel{padding:.85rem;margin-top:.75rem}strong{font-size:1.6rem}.section-title{align-items:stretch;flex-direction:column}.section-title button,.controls button,.inline-form button,.inline-form input{width:100%}.controls,.inline-form,.row-actions{align-items:stretch;flex-direction:column}.info{grid-template-columns:1fr;gap:.1rem}.info dt{margin-top:.5rem}.table-scroll{margin-left:-.85rem;margin-right:-.85rem;padding:0 .85rem}.table-scroll table{min-width:38rem}.jobs-table{display:none}.job-cards{display:block}.job-card .file-name{font-size:1rem;line-height:1.25;overflow-wrap:anywhere;word-break:break-word}.job-card .row-actions button{width:100%}dialog{width:calc(100vw - 1rem);max-width:calc(100vw - 1rem);padding:.75rem}fieldset{padding:.75rem}input{width:100%}}
|
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}
|
||||||
|
@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}}
|
||||||
|
|||||||
@@ -8,18 +8,33 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<div><h1>Importarr</h1><p>Manual SABnzbd imports, safely gated by SAB completion.</p></div>
|
<div class="brand"><h1>Importarr</h1><p id="ready-state">{{ 'Running' if status.control.queue_mode == 'start' else status.control.queue_mode|capitalize }}</p></div>
|
||||||
<div class="build"><strong>{{ status.build.version }}</strong><span>{{ status.build.git_sha[:12] }} · {{ status.build.build_date }}</span><button type="button" id="open-settings">Settings</button></div>
|
<div class="top-status"><span>Current</span><strong id="top-current-job">{{ status.current or 'idle' }}</strong></div>
|
||||||
|
<div class="top-controls">
|
||||||
|
<button type="button" data-control="start" aria-label="Start imports">▶</button>
|
||||||
|
<button type="button" data-control="pause" aria-label="Pause imports">⏸</button>
|
||||||
|
<details class="menu">
|
||||||
|
<summary aria-label="Open menu">☰</summary>
|
||||||
|
<div class="menu-panel">
|
||||||
|
<button type="button" id="open-settings">Settings</button>
|
||||||
|
<button type="button" data-control="stop">Stop queue</button>
|
||||||
|
<button type="button" data-control="cancel-current" class="danger">Cancel current job</button>
|
||||||
|
<button id="force-run" type="button">Force run now</button>
|
||||||
|
<a href="#manual-batches">Manual batches</a>
|
||||||
|
<a href="#service-info">Service info</a>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<section class="cards">
|
<section class="summary-strip" aria-label="Importarr summary">
|
||||||
<article><strong>{{ status.imported_total }}</strong><span>Imported total</span></article>
|
<article><strong>{{ status.control.queue_mode }}</strong><span>Queue mode</span></article>
|
||||||
<article><strong>{{ status.failed_total }}</strong><span>Failed total</span></article>
|
<article><strong>{{ status.current or 'Idle' }}</strong><span>Current import</span></article>
|
||||||
<article><strong>{{ status.manual_batches }}</strong><span>Manual batches</span></article>
|
<article><strong>{{ status.imported_total }}</strong><span>Imported</span></article>
|
||||||
<article><strong>{{ status.category }}</strong><span>SAB category</span></article>
|
<article><strong>{{ status.failed_total }}</strong><span>Failed</span></article>
|
||||||
</section>
|
</section>
|
||||||
<section class="panel">
|
<details class="panel packed" id="service-info">
|
||||||
<h2>Service info</h2>
|
<summary>Service info and build details</summary>
|
||||||
<dl class="info">
|
<dl class="info">
|
||||||
<dt>Name</dt><dd>{{ status.build.name }}</dd>
|
<dt>Name</dt><dd>{{ status.build.name }}</dd>
|
||||||
<dt>Version</dt><dd>{{ status.build.version }}</dd>
|
<dt>Version</dt><dd>{{ status.build.version }}</dd>
|
||||||
@@ -38,9 +53,9 @@
|
|||||||
<dt>Queue mode</dt><dd id="queue-mode">{{ status.control.queue_mode }}</dd>
|
<dt>Queue mode</dt><dd id="queue-mode">{{ status.control.queue_mode }}</dd>
|
||||||
<dt>Current job</dt><dd id="current-job">{{ status.current or 'idle' }}</dd>
|
<dt>Current job</dt><dd id="current-job">{{ status.current or 'idle' }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</section>
|
</details>
|
||||||
<section class="panel">
|
<details class="panel packed">
|
||||||
<h2>Queue controls</h2>
|
<summary>Queue controls</summary>
|
||||||
<p>Pause and stop prevent new jobs from being added to the queue. They do not interrupt an import already in progress; use cancel current job for that.</p>
|
<p>Pause and stop prevent new jobs from being added to the queue. They do not interrupt an import already in progress; use cancel current job for that.</p>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button type="button" data-control="start">Start</button>
|
<button type="button" data-control="start">Start</button>
|
||||||
@@ -48,9 +63,9 @@
|
|||||||
<button type="button" data-control="stop">Stop</button>
|
<button type="button" data-control="stop">Stop</button>
|
||||||
<button type="button" data-control="cancel-current" class="danger">Cancel current job</button>
|
<button type="button" data-control="cancel-current" class="danger">Cancel current job</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</details>
|
||||||
<section>
|
<details class="panel packed" id="manual-batches">
|
||||||
<h2>Manual batches</h2>
|
<summary>Manual batches</summary>
|
||||||
<form id="batch-form" class="inline-form">
|
<form id="batch-form" class="inline-form">
|
||||||
<input name="path" placeholder="folder under download root">
|
<input name="path" placeholder="folder under download root">
|
||||||
<input id="batch-picker" type="file" webkitdirectory directory multiple hidden>
|
<input id="batch-picker" type="file" webkitdirectory directory multiple hidden>
|
||||||
@@ -60,10 +75,9 @@
|
|||||||
<div class="table-scroll"><table><thead><tr><th>ID</th><th>Status</th><th>Path</th></tr></thead><tbody>
|
<div class="table-scroll"><table><thead><tr><th>ID</th><th>Status</th><th>Path</th></tr></thead><tbody>
|
||||||
{% for batch in batches %}<tr><td>{{ batch.id }}</td><td>{{ batch.status }}</td><td>{{ batch.path }}</td></tr>{% endfor %}
|
{% for batch in batches %}<tr><td>{{ batch.id }}</td><td>{{ batch.status }}</td><td>{{ batch.path }}</td></tr>{% endfor %}
|
||||||
</tbody></table></div>
|
</tbody></table></div>
|
||||||
</section>
|
</details>
|
||||||
<section class="panel">
|
<section class="panel queue-panel">
|
||||||
<div class="section-title"><h2>Jobs and queue</h2><button id="force-run" type="button">Force run now</button></div>
|
<div class="section-title"><h2>Queue and history</h2><span>Grouped by processing state</span></div>
|
||||||
<p>Rows are grouped by processing state. Failed and skipped rows can be retried; ignore and remove actions only update Importarr's queue.</p>
|
|
||||||
<div id="jobs">Loading…</div>
|
<div id="jobs">Loading…</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -96,8 +110,8 @@
|
|||||||
const esc=value=>String(value??'').replace(/[&<>"']/g,ch=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));
|
const esc=value=>String(value??'').replace(/[&<>"']/g,ch=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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}">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 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>Release / folder 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 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(''); }
|
||||||
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){ document.getElementById('queue-mode').textContent=d.control.queue_mode; document.getElementById('current-job').textContent=d.control.current||'idle'; } }
|
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(); }));
|
||||||
document.getElementById('browse-batch').addEventListener('click',()=>document.getElementById('batch-picker').click());
|
document.getElementById('browse-batch').addEventListener('click',()=>document.getElementById('batch-picker').click());
|
||||||
|
|||||||
+18
-1
@@ -46,6 +46,21 @@ def test_index_renders_queue_controls(tmp_path, monkeypatch):
|
|||||||
assert "cancel-current" in response.text
|
assert "cancel-current" in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_packs_secondary_controls_into_menu(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "state.db"))
|
||||||
|
import importarr.main as main
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
response = TestClient(main.app).get("/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert 'class="menu"' in response.text
|
||||||
|
assert "Service info and build details" in response.text
|
||||||
|
assert "Queue and history" in response.text
|
||||||
|
assert "Current import" in response.text
|
||||||
|
|
||||||
|
|
||||||
def test_index_renders_settings_dialog(tmp_path, monkeypatch):
|
def test_index_renders_settings_dialog(tmp_path, monkeypatch):
|
||||||
monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "state.db"))
|
monkeypatch.setenv("IMPORTARR_STATE_PATH", str(tmp_path / "state.db"))
|
||||||
import importarr.main as main
|
import importarr.main as main
|
||||||
@@ -93,8 +108,10 @@ 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:48%}" in css
|
assert ".jobs-table th:nth-child(1){width:52%}" in css
|
||||||
assert ".jobs-table .row-actions button{width:100%}" in css
|
assert ".jobs-table .row-actions button{width:100%}" in css
|
||||||
|
assert ".summary-strip" in css
|
||||||
|
assert ".menu-panel" in css
|
||||||
|
|
||||||
|
|
||||||
def test_settings_endpoint_persists_arr_connection_values(tmp_path, monkeypatch):
|
def test_settings_endpoint_persists_arr_connection_values(tmp_path, monkeypatch):
|
||||||
|
|||||||
Reference in New Issue
Block a user