webui: automation settings panel, set-location, responsive layout

- Automation tab: settings form (define_settings) beside the editor,
  stacking above it when narrow; script/settings save split.
- Set location modal: current location + reset checkbox.
- saveScriptSettings/setSavePath API wiring; plugins panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ookami125 2026-06-21 23:22:00 -04:00
parent 5a5b5f4330
commit 9a1d67acd4
11 changed files with 741 additions and 13 deletions

95
public/plugins/health.js Normal file
View file

@ -0,0 +1,95 @@
window.Naut.registerPlugin({
id: 'health',
name: 'Health Overview',
views: [{
id: 'health',
label: 'Health',
render(host, { state, f }) {
const torrents = state.snapshot.torrents;
const lowAvailability = torrents.filter((t) => t.availability < 1.05);
const idleComplete = torrents.filter((t) => t.progress >= 1 && t.upspeed === 0);
const slowDownloads = torrents.filter((t) => t.progress < 1 && t.dlspeed < 32 * 1024);
const rows = [...lowAvailability, ...slowDownloads]
.filter((t, i, arr) => arr.findIndex((x) => x.hash === t.hash) === i)
.sort((a, b) => a.availability - b.availability)
.slice(0, 18);
host.innerHTML = `<div class="pane">
<div class="settings-grid">
${metric('Low availability', lowAvailability.length)}
${metric('Idle completed', idleComplete.length)}
${metric('Slow downloads', slowDownloads.length)}
</div>
<div class="card">
<h3>Torrents needing attention</h3>
${rows.length ? table(rows, f) : '<div class="empty" style="height:120px">No health issues in the current snapshot</div>'}
</div>
</div>`;
},
}],
sidebarSections: [{
id: 'health',
title: 'Health',
render({ state }) {
const torrents = state.snapshot.torrents;
const attention = torrents.filter((t) => t.availability < 1.05 || (t.progress < 1 && t.dlspeed < 32 * 1024)).length;
return `<div class="side-item" data-health-open>
<span class="ic">+</span><span class="lbl">Needs attention</span><span class="cnt">${attention}</span>
</div>`;
},
onMount(root, { setView }) {
root.querySelector('[data-health-open]')?.addEventListener('click', () => setView('health'));
},
}],
detailTabs: [{
id: 'health-detail',
label: 'Health',
async render(host, { api, hash, f }) {
const t = await api.torrent(hash);
host.innerHTML = `<div class="props">
<div class="section-h">Health</div>
${prop('Availability', t.availability.toFixed(3))}
${prop('Connected seeds', `${t.seeds} / ${t.seedsTotal}`)}
${prop('Connected peers', `${t.peers} / ${t.peersTotal}`)}
${prop('Last activity', f.ago(t.lastActivity))}
${prop('Transfer state', t.dlspeed || t.upspeed ? 'active' : 'idle')}
</div>`;
},
}],
detailPanels: [{
id: 'health-summary',
title: 'Plugin: Health',
render(t, { f }) {
const flags = [];
if (t.availability < 1.05) flags.push('low availability');
if (t.progress < 1 && t.dlspeed < 32 * 1024) flags.push('slow download');
if (t.progress >= 1 && t.upspeed === 0) flags.push('idle seeding');
return `${prop('Attention', flags.length ? flags.join(', ') : 'none')}
${prop('Last activity', f.ago(t.lastActivity))}`;
},
}],
});
function metric(label, value) {
return `<div class="card"><h3>${label}</h3><div class="plugin-metric">${value}</div></div>`;
}
function prop(k, v) {
return `<div class="prop"><span class="k">${k}</span><span class="v">${v}</span></div>`;
}
function table(rows, f) {
return `<table class="dtbl"><thead><tr>
<th>Name</th><th class="num">Done</th><th class="num">Availability</th><th class="num">Down</th><th class="num">Up</th>
</tr></thead><tbody>${rows.map((t) => `<tr>
<td>${f.esc(t.name)}</td>
<td class="num">${f.pct(t.progress)}</td>
<td class="num">${t.availability.toFixed(2)}</td>
<td class="num">${f.rate(t.dlspeed)}</td>
<td class="num">${f.rate(t.upspeed)}</td>
</tr>`).join('')}</tbody></table>`;
}

View file

@ -0,0 +1,5 @@
{
"modules": [
"/plugins/health.js"
]
}