Show per-indexer grab share and rebuild the Indexers card as a list
All checks were successful
Deploy / deploy (push) Successful in 26s

The Indexers card sat beside Requests as a stretched, vertically centred
grid of pills — mostly dead space once the Ombi list was expanded, and it
said nothing about which indexer was actually doing the work.

- Fetch Prowlarr's indexerstats over a rolling 30-day window and merge
  per-indexer queries/grabs into the summary. Cached 10 min on success and
  never on failure, so a stats outage can't blank the column after it
  recovers or take the up/down list down with it.
- Rank indexers busiest-first by grab share (query share as fallback);
  queries mostly count enabled indexers, grabs count who delivered.
- One indexer per line, matched to the Ombi request rows: same padding,
  gap, radius and two-line body, plus a share bar scaled to the busiest
  indexer and an Online/Offline badge the 30s poll keeps current.
- Let that row size cards to their own content instead of stretching.

Bump APP_VERSION to 1.2.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Barely Removable 2026-08-26 22:28:39 -07:00
parent 2cc62979a2
commit 909a0b568c
4 changed files with 147 additions and 16 deletions

View file

@ -163,7 +163,10 @@ main {
}
.card-body { padding: 1.1rem; flex: 1; }
.card-body > :first-child { margin-top: 0; }
#prowlarr-body { display: flex; flex-direction: column; justify-content: center; }
/* Requests can grow tall when expanded; the Indexers card beside it should
keep its natural height and sit at the top rather than stretching into a
mostly-empty box with its content floating in the middle. */
.row.align-start { align-items: start; }
/* ── "Right now" hero strip ─────────────────────────── */
.hero .card { border-color: var(--border-lt); }
@ -431,26 +434,52 @@ dd.is-accent { color: var(--accent); }
}
/* ── Indexers ───────────────────────────────────────── */
.indexer-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 0.65rem;
}
/* One indexer per line, built to the same rhythm as the Ombi request rows
beside it — same padding, gap, radius and two-line body, so the two cards
read as one pair rather than two unrelated layouts. */
.indexer-grid { display: flex; flex-direction: column; gap: 0.5rem; }
.indexer-item {
display: flex;
align-items: center;
gap: 0.65rem;
min-height: var(--tap);
padding: 0.75rem 0.9rem;
gap: 0.7rem;
padding: 0.6rem 0.7rem;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-size: var(--fs-base);
min-width: 0;
}
.indexer-item.down { border-color: var(--danger-border); }
.indexer-item .status-light { width: 9px; height: 9px; }
.indexer-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text-2); }
.indexer-item .status-light { width: 9px; height: 9px; flex-shrink: 0; }
.indexer-info { min-width: 0; flex: 1; }
.indexer-name {
font-size: var(--fs-sm); font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.indexer-sub { font-size: var(--fs-xs); color: var(--text-3); }
.indexer-sub .proto { text-transform: capitalize; }
.indexer-sub:empty::before { content: "Indexer"; }
/* Share of grabs, scaled to the busiest indexer (see dashboard.html) */
.share-bar {
margin-top: 0.35rem;
height: 4px;
border-radius: 100px;
background: var(--surface);
overflow: hidden;
}
.share-bar span {
display: block;
height: 100%;
border-radius: 100px;
background: var(--accent);
}
.indexer-item.down .share-bar span { background: var(--text-3); }
.share-pct {
font-size: var(--fs-sm);
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--text-2);
flex-shrink: 0;
}
/* ── Requests (Ombi) ────────────────────────────────── */
.req-list { display: flex; flex-direction: column; gap: 0.5rem; }
@ -479,6 +508,8 @@ dd.is-accent { color: var(--accent); }
.badge.denied { background: var(--danger-dim); color: var(--danger); }
.badge.idle { background: var(--surface); color: var(--text-3); }
.badge.downloading { background: var(--accent-dim); color: var(--accent); }
.badge.online { background: var(--ok-dim); color: var(--ok); }
.badge.offline { background: var(--danger-dim); color: var(--danger); }
.badge.paused { background: var(--warn-dim); color: var(--warn); }
.req-info { min-width: 0; flex: 1; }
.req-title {

View file

@ -187,6 +187,12 @@ async function pollProwlarr() {
const light = document.getElementById('idx-light-' + idx.id);
if (row) { row.classList.toggle('up', idx.up); row.classList.toggle('down', !idx.up); }
if (light) { light.classList.toggle('up', idx.up); light.classList.toggle('down', !idx.up); }
const badge = document.getElementById('idx-badge-' + idx.id);
if (badge) {
badge.classList.toggle('online', idx.up);
badge.classList.toggle('offline', !idx.up);
badge.textContent = idx.up ? 'Online' : 'Offline';
}
});
updateServicePill('prowlarr', d.up_count === d.total && d.total > 0);
const stat = document.getElementById('svc-stat-prowlarr');