adding availability
Some checks failed
Build and Deploy (internal) / Build Transmission Manager Image (push) Failing after 0s
Build and Deploy (internal) / Deploy Transmission Manager (internal) (push) Has been skipped

This commit is contained in:
2026-07-08 20:01:02 -03:00
parent a57002bf44
commit 7d1b868107
4 changed files with 59 additions and 10 deletions

View File

@@ -162,7 +162,7 @@ var torrentFields = []string{
"id", "name", "totalSize", "downloadedEver", "uploadedEver", "uploadRatio", "id", "name", "totalSize", "downloadedEver", "uploadedEver", "uploadRatio",
"percentDone", "status", "eta", "error", "errorString", "doneDate", "isFinished", "percentDone", "status", "eta", "error", "errorString", "doneDate", "isFinished",
"rateDownload", "rateUpload", "peersConnected", "peersGettingFromUs", "peersSendingToUs", "rateDownload", "rateUpload", "peersConnected", "peersGettingFromUs", "peersSendingToUs",
"peers", "availability", "peers",
} }
type rpcRequest struct { type rpcRequest struct {

View File

@@ -56,13 +56,15 @@ Torrent list requests should include:
id, name, totalSize, downloadedEver, uploadedEver, uploadRatio, id, name, totalSize, downloadedEver, uploadedEver, uploadRatio,
percentDone, status, eta, error, errorString, doneDate, isFinished, percentDone, status, eta, error, errorString, doneDate, isFinished,
rateDownload, rateUpload, peersConnected, peersGettingFromUs, peersSendingToUs, rateDownload, rateUpload, peersConnected, peersGettingFromUs, peersSendingToUs,
peers availability, peers
``` ```
Peer objects are expected to include `address`, `clientName`, `progress`, `isDownloadingFrom`, `isUploadingTo`, `rateToClient`, and `rateToPeer` when available. Peer objects are expected to include `address`, `clientName`, `progress`, `isDownloadingFrom`, `isUploadingTo`, `rateToClient`, and `rateToPeer` when available.
The UI definition of "peers at 100%" is an absolute count of `peers[]` entries whose `progress === 1.0`. Do not reinterpret it as a percentage of connected peers. The UI definition of "peers at 100%" is an absolute count of `peers[]` entries whose `progress === 1.0`. Do not reinterpret it as a percentage of connected peers.
The UI availability value comes from Transmission's `availability` per-piece array. Render and sort it as a complete-copy estimate, treating `-1` entries as pieces available locally.
## Frontend Architecture ## Frontend Architecture
The frontend is a single page with module-level state in `web/app.js`. It polls on a user-selectable interval, defaults to 3 seconds, and fetches torrents and stats in parallel. The frontend is a single page with module-level state in `web/app.js`. It polls on a user-selectable interval, defaults to 3 seconds, and fetches torrents and stats in parallel.
@@ -84,7 +86,7 @@ Current sort keys:
- `ratio` - `ratio`
- `size` - `size`
- `speed`: orders by combined download and upload speed descending, then download speed, upload speed, and name. - `speed`: orders by combined download and upload speed descending, then download speed, upload speed, and name.
- `peer100`: orders by peers at 100% descending, then connected peers descending, progress descending, and name ascending. - `availability`: orders by computed availability descending, then progress, then name.
## UI And Design Rules ## UI And Design Rules

View File

@@ -250,9 +250,50 @@ function activitySpeed(t) {
return (t.rateDownload || 0) + (t.rateUpload || 0); return (t.rateDownload || 0) + (t.rateUpload || 0);
} }
function availabilityValue(t) {
if (typeof t.availability === "number" && Number.isFinite(t.availability)) {
return t.availability;
}
const pieces = Array.isArray(t.availability) ? t.availability : [];
if (!pieces.length) return null;
const counts = [];
let min = Infinity;
for (const piece of pieces) {
const raw = Number(piece);
if (!Number.isFinite(raw)) continue;
const count = raw < 0 ? 1 : raw;
counts.push(count);
if (count < min) min = count;
}
if (!counts.length) return null;
let aboveMin = 0;
for (const count of counts) {
if (count > min) aboveMin += 1;
}
return min + (aboveMin / counts.length);
}
function availabilitySortValue(t) {
const value = availabilityValue(t);
return value == null ? -1 : value;
}
function humanAvailability(value) {
if (value == null || !Number.isFinite(value) || value < 0) return "—";
return `${value.toFixed(value < 10 ? 2 : 1)}x`;
}
function sortTorrents(list) { function sortTorrents(list) {
const key = state.sortKey; const key = state.sortKey;
const sorted = list.slice(); const sorted = list.slice();
const availabilityValues = key === "availability"
? new Map(sorted.map((t) => [t, availabilitySortValue(t)]))
: null;
sorted.sort((a, b) => { sorted.sort((a, b) => {
switch (key) { switch (key) {
case "name": case "name":
@@ -268,14 +309,10 @@ function sortTorrents(list) {
|| ((b.rateDownload || 0) - (a.rateDownload || 0)) || ((b.rateDownload || 0) - (a.rateDownload || 0))
|| ((b.rateUpload || 0) - (a.rateUpload || 0)) || ((b.rateUpload || 0) - (a.rateUpload || 0))
|| compareName(a, b); || compareName(a, b);
case "peer100": { case "availability":
const pa = peerStatus(a); return (availabilityValues.get(b) - availabilityValues.get(a))
const pb = peerStatus(b);
return (pb.at100 - pa.at100)
|| (pb.connected - pa.connected)
|| ((b.percentDone || 0) - (a.percentDone || 0)) || ((b.percentDone || 0) - (a.percentDone || 0))
|| compareName(a, b); || compareName(a, b);
}
case "status": { case "status": {
const ra = STATUS_RANK[classify(a).key] ?? 99; const ra = STATUS_RANK[classify(a).key] ?? 99;
const rb = STATUS_RANK[classify(b).key] ?? 99; const rb = STATUS_RANK[classify(b).key] ?? 99;
@@ -359,6 +396,10 @@ function createCard() {
<span class="k">ETA</span> <span class="k">ETA</span>
<span class="v" data-field="eta"></span> <span class="v" data-field="eta"></span>
</div> </div>
<div class="stat-cell">
<span class="k">Availability</span>
<span class="v mono" data-field="availability"></span>
</div>
<div class="stat-cell"> <div class="stat-cell">
<span class="k">Peers @ 100%</span> <span class="k">Peers @ 100%</span>
<span class="v mono" data-field="peers-100"></span> <span class="v mono" data-field="peers-100"></span>
@@ -397,6 +438,7 @@ function updateCard(card, t) {
const pctText = (pct * 100).toFixed(pct === 1 ? 0 : 1) + "%"; const pctText = (pct * 100).toFixed(pct === 1 ? 0 : 1) + "%";
const isDone = pct >= 1.0; const isDone = pct >= 1.0;
const ps = peerStatus(t); const ps = peerStatus(t);
const availability = availabilityValue(t);
const dl = t.rateDownload || 0; const dl = t.rateDownload || 0;
const ul = t.rateUpload || 0; const ul = t.rateUpload || 0;
const eta = (dl > 0 && !isDone) ? formatETA(t.eta) : (isDone ? "—" : "∞"); const eta = (dl > 0 && !isDone) ? formatETA(t.eta) : (isDone ? "—" : "∞");
@@ -444,6 +486,11 @@ function updateCard(card, t) {
setValueClass(ulEl, ul > 0 ? "success" : "dim"); setValueClass(ulEl, ul > 0 ? "success" : "dim");
setText(field(card, "eta"), eta); setText(field(card, "eta"), eta);
const availabilityEl = field(card, "availability");
setText(availabilityEl, humanAvailability(availability));
setValueClass(availabilityEl, availability == null ? "dim" : availability >= 1 ? "success" : "warn");
setText(field(card, "peers-100"), `${ps.at100}/${ps.connected}`); setText(field(card, "peers-100"), `${ps.at100}/${ps.connected}`);
setText(field(card, "finished"), formatDate(t.doneDate)); setText(field(card, "finished"), formatDate(t.doneDate));

View File

@@ -54,7 +54,7 @@
<button class="sort-btn" data-sort="ratio">Ratio</button> <button class="sort-btn" data-sort="ratio">Ratio</button>
<button class="sort-btn" data-sort="size">Size</button> <button class="sort-btn" data-sort="size">Size</button>
<button class="sort-btn" data-sort="speed">Speed</button> <button class="sort-btn" data-sort="speed">Speed</button>
<button class="sort-btn" data-sort="peer100">100% Peers</button> <button class="sort-btn" data-sort="availability">Availability</button>
</div> </div>
<div class="toolbar-actions"> <div class="toolbar-actions">
<label class="interval-control" for="refresh-interval"> <label class="interval-control" for="refresh-interval">