- 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>
95 lines
3.5 KiB
JavaScript
95 lines
3.5 KiB
JavaScript
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>`;
|
|
}
|