arr-summary/static/css/dashboard.css
Barely Removable 909a0b568c
All checks were successful
Deploy / deploy (push) Successful in 26s
Show per-indexer grab share and rebuild the Indexers card as a list
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>
2026-08-26 22:28:39 -07:00

640 lines
19 KiB
CSS

/* ── Header ─────────────────────────────────────────── */
/* Two deliberate rows at every width: identity + controls on top,
service pills on their own full-width row beneath. Letting the pills
share row one made the header wrap into ragged rows and orphan the
clock as soon as the six of them stopped fitting. Scrolls away with
the page — a status bar isn't worth permanent screen real estate. */
header {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 0.75rem 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem var(--gap);
flex-wrap: wrap;
}
.brand {
order: 1;
display: flex;
align-items: center;
gap: 0.65rem;
min-width: 0;
flex: 0 0 auto;
}
.brand svg { flex-shrink: 0; display: block; }
.brand h1 {
margin: 0;
font-size: var(--fs-lg);
font-weight: 600;
letter-spacing: -0.01em;
white-space: nowrap;
}
/* Service status pills — own row, wrapping among themselves so every
service stays visible at any width. Never scroll or clip: a hidden
pill is a service whose status you can't see. */
.svc-strip {
order: 3;
display: flex;
align-items: center;
gap: 0.4rem;
flex: 0 0 100%;
flex-wrap: wrap;
justify-content: center;
min-width: 0;
}
.svc-pill {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.35rem 0.7rem;
min-height: 34px;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 100px;
font-size: var(--fs-xs);
color: var(--text-2);
text-decoration: none;
white-space: nowrap;
transition: border-color 0.2s, color 0.2s;
}
.svc-pill:hover { border-color: var(--border-lt); color: var(--text); }
.svc-pill.down { color: var(--danger); border-color: var(--danger-border); }
.svc-name { font-weight: 600; }
.svc-stat { color: var(--text-3); }
.svc-pill.down .svc-stat { color: var(--danger); }
.status-light {
width: 7px; height: 7px; border-radius: 50%;
flex-shrink: 0; background: var(--text-3);
}
.status-light.up { background: var(--ok); box-shadow: 0 0 0 3px var(--ok-dim); }
.status-light.down { background: var(--danger); box-shadow: 0 0 0 3px var(--danger-dim); }
.header-right {
order: 2;
display: flex;
align-items: center;
gap: 0.75rem;
margin-left: auto;
flex: 0 0 auto;
}
.header-clock {
font-variant-numeric: tabular-nums;
font-size: var(--fs-sm);
color: var(--text-2);
white-space: nowrap;
}
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
min-width: var(--tap);
min-height: var(--tap);
padding: 0 0.85rem;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-2);
font-size: var(--fs-sm);
font-family: inherit;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
transition: color 0.2s, border-color 0.2s, background 0.2s;
}
.icon-btn:hover { color: var(--text); border-color: var(--border-lt); background: var(--surface); }
/* ── Layout ─────────────────────────────────────────── */
main {
padding: var(--gap-lg);
max-width: 1360px;
margin: 0 auto;
}
.section {
margin-bottom: 2rem;
}
.section > h2 {
margin: 0 0 0.85rem;
font-size: var(--fs-xs);
font-weight: 600;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--text-3);
}
.row {
display: grid;
gap: var(--gap);
}
.row.cols-3 { grid-template-columns: repeat(3, 1fr); }
.row.cols-2 { grid-template-columns: repeat(2, 1fr); }
/* ── Card ───────────────────────────────────────────── */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
display: flex;
flex-direction: column;
min-width: 0;
}
.card-header {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.85rem 1.1rem;
border-bottom: 1px solid var(--border);
}
.card-header h3 {
margin: 0;
font-size: var(--fs-sm);
font-weight: 600;
color: var(--text);
}
.card-header h3 a { color: inherit; text-decoration: none; }
.card-header h3 a:hover { color: var(--accent); }
.card-meta {
margin-left: auto;
font-size: var(--fs-xs);
color: var(--text-3);
white-space: nowrap;
}
.card-body { padding: 1.1rem; flex: 1; }
.card-body > :first-child { margin-top: 0; }
/* 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); }
.hero-figure {
font-size: var(--fs-2xl);
font-weight: 600;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
line-height: 1.1;
}
.hero-sub {
font-size: var(--fs-sm);
color: var(--text-3);
margin-top: 0.3rem;
}
/* Now-playing list */
.stream {
display: flex;
align-items: center;
gap: 0.65rem;
padding: 0.55rem 0;
border-top: 1px solid var(--border);
}
.stream:first-child { border-top: none; padding-top: 0; }
.stream-icon { font-size: var(--fs-base); flex-shrink: 0; }
.stream-info { min-width: 0; flex: 1; }
.stream-title {
font-size: var(--fs-sm); font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.stream-user { font-size: var(--fs-xs); color: var(--text-3); }
.live-dot {
width: 7px; height: 7px; border-radius: 50%;
background: var(--ok); flex-shrink: 0;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
@media (prefers-reduced-motion: reduce) {
.live-dot { animation: none; }
* { transition-duration: 0.01ms !important; }
}
/* ── Meter (storage / progress) ─────────────────────── */
.meter {
height: 7px;
background: var(--surface-2);
border-radius: 4px;
overflow: hidden;
margin-top: 0.65rem;
}
.meter-fill {
height: 100%;
border-radius: 4px;
background: var(--ok);
transition: width 0.5s ease;
}
.meter-fill.warn { background: var(--warn); }
.meter-fill.danger { background: var(--danger); }
.meter-legend {
display: flex;
justify-content: space-between;
font-size: var(--fs-xs);
color: var(--text-3);
margin-top: 0.45rem;
}
/* ── Stats (semantic definition list) ───────────────── */
dl.stats {
display: grid;
gap: 0.6rem;
margin: 0;
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
}
dl.stats > div {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 0.65rem 0.75rem;
min-width: 0;
}
dl.stats dt {
font-size: var(--fs-xs);
color: var(--text-3);
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
dl.stats dd {
margin: 0.2rem 0 0;
font-size: var(--fs-lg);
font-weight: 600;
font-variant-numeric: tabular-nums;
}
dd.is-warn { color: var(--warn); }
dd.is-danger { color: var(--danger); }
dd.is-ok { color: var(--ok); }
dd.is-accent { color: var(--accent); }
/* ── Buttons ────────────────────────────────────────── */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: var(--tap);
padding: 0 1rem;
border: 1px solid transparent;
border-radius: var(--radius-sm);
background: var(--accent);
color: var(--bg);
font-family: inherit;
font-size: var(--fs-sm);
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: opacity 0.15s, background 0.15s;
}
.btn:hover:not(:disabled) { opacity: 0.88; }
.btn:disabled { opacity: 0.5; cursor: default; }
.btn-quiet {
background: var(--surface-2);
border-color: var(--border);
color: var(--text-2);
}
.btn-quiet:hover:not(:disabled) { color: var(--text); border-color: var(--border-lt); }
/* Compact add button — still meets 44px on touch (see media query) */
.btn-add {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 32px;
padding: 0 0.7rem;
border: 1px solid var(--border-lt);
border-radius: var(--radius-sm);
background: var(--surface-2);
color: var(--text);
font-family: inherit;
font-size: var(--fs-xs);
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.btn-add:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
/* Armed = awaiting the confirming second tap */
.btn-add.armed {
background: var(--warn);
border-color: var(--warn);
color: var(--bg);
}
.btn-add.done {
background: var(--ok-dim);
border-color: transparent;
color: var(--ok);
cursor: default;
}
.btn-add:disabled { cursor: default; }
/* ── Search ─────────────────────────────────────────── */
.search-bar { display: flex; gap: 0.5rem; margin-bottom: 0.85rem; }
.search-bar input {
flex: 1;
min-width: 0;
min-height: var(--tap);
padding: 0 0.85rem;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text);
font-family: inherit;
font-size: var(--fs-sm);
}
.search-bar input::placeholder { color: var(--text-3); }
.search-bar input:focus { border-color: var(--accent); outline: none; }
.search-result {
display: none;
align-items: center;
gap: 0.75rem;
padding: 0.7rem 0.85rem;
margin-bottom: 0.85rem;
background: var(--accent-dim);
border: 1px solid rgba(122,162,247,0.3);
border-radius: var(--radius-sm);
}
.search-result.visible { display: flex; }
.search-result-info { min-width: 0; flex: 1; }
.search-result-title {
font-size: var(--fs-sm); font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.search-result-meta { font-size: var(--fs-xs); color: var(--text-3); }
.search-msg {
font-size: var(--fs-sm);
color: var(--text-2);
padding: 0.5rem 0;
}
.search-msg.error { color: var(--danger); }
/* ── Discover (RT) list ─────────────────────────────── */
.rt-list { display: flex; flex-direction: column; gap: 0.5rem; }
.rt-item {
display: flex;
align-items: center;
gap: 0.7rem;
padding: 0.6rem;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
transition: border-color 0.2s, opacity 0.3s, transform 0.3s;
}
.rt-item:hover { border-color: var(--border-lt); }
.rt-item.removing { opacity: 0; transform: scale(0.97); pointer-events: none; }
.rt-item.is-hidden { display: none; }
.rt-rank {
font-size: var(--fs-xs);
color: var(--text-3);
font-variant-numeric: tabular-nums;
width: 1.2rem;
text-align: right;
flex-shrink: 0;
}
.rt-poster {
width: 38px; height: 56px;
object-fit: cover;
border-radius: 4px;
background: var(--surface);
flex-shrink: 0;
}
.rt-info { min-width: 0; flex: 1; }
.rt-title {
font-size: var(--fs-sm);
font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rt-year { color: var(--text-3); font-weight: 400; }
.rt-meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
font-size: var(--fs-xs);
margin-top: 0.15rem;
}
.score.certified { color: var(--ok); }
.score.critics { color: var(--warn); }
.score.audience { color: var(--text-3); }
/* Description is inline + clamped — readable on touch, no hover required */
.rt-desc {
font-size: var(--fs-xs);
color: var(--text-3);
margin-top: 0.25rem;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.rt-desc:empty { display: none; }
.show-more {
margin-top: 0.6rem;
width: 100%;
min-height: 38px;
}
/* ── Indexers ───────────────────────────────────────── */
/* 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.7rem;
padding: 0.6rem 0.7rem;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
min-width: 0;
}
.indexer-item.down { border-color: var(--danger-border); }
.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; }
.req-item.is-hidden { display: none; }
.req-item {
display: flex;
align-items: center;
gap: 0.7rem;
padding: 0.6rem 0.7rem;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.badge {
font-size: var(--fs-xs);
font-weight: 600;
padding: 0.2rem 0.5rem;
border-radius: 100px;
white-space: nowrap;
flex-shrink: 0;
}
.badge.movie { background: var(--accent-dim); color: var(--accent); }
.badge.tv { background: var(--ok-dim); color: var(--ok); }
.badge.pending { background: var(--warn-dim); color: var(--warn); }
.badge.available, .badge.approved { background: var(--ok-dim); color: var(--ok); }
.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 {
font-size: var(--fs-sm); font-weight: 600;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.req-sub { font-size: var(--fs-xs); color: var(--text-3); }
/* ── Insights top lists ─────────────────────────────── */
.top-list { display: flex; flex-direction: column; gap: 0.4rem; }
.top-item {
display: flex;
align-items: center;
gap: 0.65rem;
min-height: 3.25rem;
padding: 0.5rem 0.65rem;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.top-rank {
font-size: var(--fs-xs);
color: var(--text-3);
font-variant-numeric: tabular-nums;
width: 1.1rem;
flex-shrink: 0;
}
.top-info { min-width: 0; flex: 1; }
.top-name {
font-size: var(--fs-sm);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.top-sub { font-size: var(--fs-xs); color: var(--text-3); }
.top-plays {
font-size: var(--fs-xs);
color: var(--text-2);
font-variant-numeric: tabular-nums;
flex-shrink: 0;
}
.empty { color: var(--text-3); font-size: var(--fs-sm); }
/* ── Errors ─────────────────────────────────────────── */
.error {
background: var(--surface-2);
border: 1px solid var(--border);
border-left: 3px solid var(--danger);
border-radius: var(--radius-sm);
padding: 0.8rem 0.9rem;
color: var(--text-2);
font-size: var(--fs-sm);
}
.error-detail { margin-top: 0.5rem; }
.error-detail summary {
cursor: pointer;
font-size: var(--fs-xs);
color: var(--text-3);
list-style: none;
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.3rem 0;
}
.error-detail summary::-webkit-details-marker { display: none; }
.error-detail summary::before { content: "▸"; }
.error-detail[open] summary::before { content: "▾"; }
.error-detail summary:hover { color: var(--text-2); }
.error-detail pre {
margin: 0.4rem 0 0;
padding: 0.6rem 0.7rem;
background: rgba(0,0,0,0.3);
border-radius: 6px;
font-family: var(--mono);
font-size: var(--fs-xs);
line-height: 1.5;
color: var(--text-3);
white-space: pre-wrap;
word-break: break-word;
overflow-x: auto;
}
footer {
padding: 1.5rem;
text-align: center;
font-size: var(--fs-xs);
color: var(--text-3);
border-top: 1px solid var(--border);
}
/* Build identity — dim, so it reads as metadata rather than content. */
footer .version {
display: block;
margin-top: 0.3rem;
font-family: var(--mono);
font-variant-numeric: tabular-nums;
opacity: 0.75;
}
.visually-hidden {
position: absolute; width: 1px; height: 1px;
padding: 0; margin: -1px; overflow: hidden;
clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* ── Responsive ─────────────────────────────────────── */
@media (max-width: 1100px) {
.row.cols-3 { grid-template-columns: 1fr 1fr; }
.row.cols-3 > :first-child { grid-column: 1 / -1; }
}
@media (max-width: 820px) {
.row.cols-2, .row.cols-3 { grid-template-columns: 1fr; }
.row.cols-3 > :first-child { grid-column: auto; }
}
@media (max-width: 700px) {
/* Row order and the full-width pill row are set in the base rules. */
header { padding: 0.7rem 1rem; gap: 0.6rem; }
.brand { flex: 1 1 auto; }
.brand h1 { font-size: var(--fs-base); }
.header-clock { display: none; }
.svc-strip { justify-content: flex-start; }
main { padding: 1rem; }
.card-body { padding: 0.9rem; }
.card-header { padding: 0.75rem 0.9rem; }
.search-bar { flex-direction: column; }
.search-bar .btn { width: 100%; }
/* Full touch targets once there's no mouse */
.btn-add { min-height: var(--tap); padding: 0 0.9rem; }
.search-bar input { font-size: 16px; } /* prevent iOS zoom on focus */
}