arr-summary/templates/dashboard.html
Barely Removable 395638aaf2 Surface parity temp in the Storage header, enlarge the settings cog
Parity went back into the GraphQL query, but for its temperature only —
it still gets no table row, since it has no filesystem to measure. The
temp rides in the card header next to the array summary, which was the
one genuinely useful thing the old parity row carried: a parity drive
cooking away is worth seeing. Multiple parity drives each get a badge,
and .meta-parity keeps the label and badge from wrapping apart.

The settings cog was a speck. The button was always a full 44px --tap
target, but the glyph inside it was set at --fs-sm (13px), so the
affordance read as far smaller than the hit area actually was. Now
--fs-xl (24px) with line-height 1 — roughly 55% of the button, which is
the usual proportion for an icon button. .icon-btn has exactly one user
(the cog), so nothing else shifts.

Verified at 1400px and 375px: no page overflow, the cog still measures
44x44, and the parity badge stays visible and intact on both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 13:19:17 -07:00

618 lines
29 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Media Centre</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.svg') }}">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}">
</head>
{#- Degraded service: human-readable cause, raw exception behind a
disclosure with any API key redacted. -#}
{% macro service_error(name, raw) %}
<div class="error" role="alert">
<div>{{ raw | friendly_error(name) }}</div>
{% if raw %}
<details class="error-detail">
<summary>Technical details</summary>
<pre>{{ raw | redact_secrets }}</pre>
</details>
{% endif %}
</div>
{% endmacro %}
{#- Disk temperature / spin state, as one badge. A spun-down disk reports no
usable temperature, so Standby replaces the reading rather than showing
a stale or zero degree value. -#}
{% macro disk_state(dk) %}
{%- if dk.spinning == false -%}
<span class="badge idle">Standby</span>
{%- elif dk.temp is not none -%}
<span class="badge temp-{% if dk.temp >= 55 %}danger{% elif dk.temp >= 45 %}warn{% else %}ok{% endif %}">{{ dk.temp }}&deg;C</span>
{%- else -%}
<span class="disk-null">&mdash;</span>
{%- endif -%}
{% endmacro %}
{#- One service status pill in the Status strip. `short` is swapped in for
`label` on phones, where six full names can't share one row without
ellipsising down to two characters — see the .svc-name rules. The full
name stays in the DOM for screen readers either way. -#}
{% macro svc_pill(key, label, short, url, up, stat) %}
<a class="svc-pill {{ 'up' if up else 'down' }}" id="svc-{{ key }}"
href="{{ url }}" target="_blank" rel="noopener" title="{{ label }}">
<span class="status-light {{ 'up' if up else 'down' }}" id="svc-light-{{ key }}"></span>
<span class="svc-name" data-short="{{ short }}">{{ label }}</span>
<span class="svc-stat" id="svc-stat-{{ key }}">{{ stat }}</span>
</a>
{% endmacro %}
<body data-timezone="{{ timezone }}" data-url-base="{{ request.script_root }}">
<header>
<div class="brand">
<svg width="30" height="30" viewBox="0 0 100 100" aria-hidden="true">
<rect width="100" height="100" rx="22" fill="var(--accent)"/>
<path d="M40 32 L72 50 L40 68 Z" fill="var(--bg)"/>
</svg>
<h1>Media Centre</h1>
</div>
<div class="header-right">
<div class="header-clock" id="header-clock">—</div>
<a href="{{ url_for('settings_page') }}" class="icon-btn" aria-label="Settings">
<span aria-hidden="true">⚙</span>
</a>
</div>
</header>
<main>
<!-- ══ Status — is everything reachable? ══ -->
<section class="section" aria-labelledby="h-status">
<h2 id="h-status">Status</h2>
{# Every service stays visible at every width — these wrap onto more
rows rather than scrolling or truncating. A hidden pill is a service
whose status you can't see. #}
<nav class="svc-strip" aria-label="Service status">
{{ svc_pill('sonarr', 'Sonarr', 'Son', service_urls.sonarr, sonarr.status == 'ok',
(sonarr.total_series ~ ' series') if sonarr.status == 'ok' else 'offline') }}
{{ svc_pill('radarr', 'Radarr', 'Rad', service_urls.radarr, radarr.status == 'ok',
(radarr.total_movies ~ ' movies') if radarr.status == 'ok' else 'offline') }}
{{ svc_pill('prowlarr', 'Prowlarr', 'Prow', service_urls.prowlarr, prowlarr.status == 'ok',
(prowlarr.up_count ~ '/' ~ prowlarr.total ~ ' up') if prowlarr.status == 'ok' else 'offline') }}
{{ svc_pill('sabnzbd', 'SABnzbd', 'SAB', service_urls.sabnzbd, sabnzbd.status == 'ok',
sabnzbd.speed if sabnzbd.status == 'ok' else 'offline') }}
{{ svc_pill('tautulli', 'Tautulli', 'Taut', service_urls.tautulli, tautulli.status == 'ok',
((tautulli.active_streams ~ ' streaming') if tautulli.active_streams > 0 else '0 streams') if tautulli.status == 'ok' else 'offline') }}
{{ svc_pill('ombi', 'Ombi', 'Ombi', service_urls.ombi, ombi.status == 'ok',
(ombi.total_pending ~ ' pending') if ombi.status == 'ok' else 'offline') }}
</nav>
</section>
<!-- ══ Right now — the time-sensitive row, above the fold ══ -->
<section class="section hero" aria-labelledby="h-now">
<h2 id="h-now">Right now</h2>
{# Two cards here, not three. Storage carries a per-disk table that no
third-of-a-page column can hold without towering over these two and
hollowing them out — it gets its own full-width row below. #}
<div class="row cols-2">
<!-- Now playing -->
<div class="card">
<div class="card-header">
<h3><a href="{{ service_urls.tautulli }}" target="_blank" rel="noopener">Now playing</a></h3>
<span class="card-meta" id="tautulli-count">
{%- if tautulli.status == 'ok' -%}
{{ tautulli.active_streams }} active
{%- endif -%}
</span>
</div>
<div class="card-body">
{% if tautulli.status == 'ok' %}
<div id="now-playing" aria-live="polite">
{% if tautulli.sessions %}
{% for s in tautulli.sessions %}
<div class="stream">
<span class="live-dot"></span>
<span class="stream-icon" aria-hidden="true">{{ '🎬' if s.media_type == 'movie' else '📺' }}</span>
<span class="stream-info">
<span class="stream-title">{{ s.title }}</span>
<span class="stream-user">{{ s.user }}</span>
</span>
</div>
{% endfor %}
{% else %}
<p class="empty">Nothing streaming right now.</p>
{% endif %}
</div>
{% else %}
{{ service_error("Tautulli", tautulli.error) }}
{% endif %}
</div>
</div>
<!-- Downloads -->
<div class="card">
<div class="card-header">
<h3><a href="{{ service_urls.sabnzbd }}" target="_blank" rel="noopener">Downloads</a></h3>
{% if sabnzbd.status == 'ok' %}
{% set st = sabnzbd.sab_status | lower %}
<span class="card-meta">
<span id="sab-badge" class="badge {{ 'downloading' if st == 'downloading' else ('paused' if st == 'paused' else 'idle') }}">{{ sabnzbd.sab_status }}</span>
</span>
{% endif %}
</div>
<div class="card-body">
{% if sabnzbd.status == 'ok' %}
<div aria-live="polite">
<div class="hero-figure" id="sab-speed">{{ sabnzbd.speed }}</div>
<div class="hero-sub">
<span id="sab-queue">{{ sabnzbd.queue_count }}</span> queued ·
<span id="sab-remaining">{{ sabnzbd.sizeleft }}</span> left
<span id="sab-timeleft-row" style="display:none;"> · ETA <span id="sab-timeleft"></span></span>
</div>
</div>
<dl class="stats">
<div><dt>Avg speed (7d)</dt><dd>{{ sabnzbd.avg_speed }}</dd></div>
<div><dt>Grabbed (7d)</dt><dd>{{ sabnzbd.completed_7d }}</dd></div>
<div><dt>Volume (7d)</dt><dd>{{ sabnzbd.week_size }}</dd></div>
</dl>
{% else %}
{{ service_error("SABnzbd", sabnzbd.error) }}
{% endif %}
</div>
</div>
</div>
{# Storage spans the page so its per-disk table has room to be a table.
Ten disks down a narrow column is a tower; across the full width it
is ten short rows. #}
<div class="row">
<div class="card">
<div class="card-header">
<h3><a href="{{ service_urls.unraid }}" target="_blank" rel="noopener">Storage</a></h3>
{% if unraid.status == 'ok' %}
<span class="card-meta card-meta-wrap">
{{ unraid.array_used_pct }}% used · {{ unraid.array_free }} free of {{ unraid.array_total }}
{%- if unraid.disk_count %} · {{ unraid.disk_count }} disk{{ 's' if unraid.disk_count != 1 }}{% endif %}
{%- if unraid.array_state %} · {{ unraid.array_state | capitalize }}{% endif %}
{#- Parity has no table row of its own (no filesystem to measure),
so its temperature rides here — the one thing worth watching. -#}
{%- if unraid.parities %} · <span class="meta-parity">Parity
{%- for dk in unraid.parities %} {{ disk_state(dk) }}{% endfor %}</span>
{%- endif %}
</span>
{% endif %}
</div>
<div class="card-body">
{% if unraid.status == 'ok' %}
{% if unraid.disks or unraid.caches %}
{# Array total leads, then the disks that make it up. #}
{% set pct = unraid.array_used_pct %}
<div class="meter array-meter">
<div class="meter-fill {% if pct > 85 %}danger{% elif pct > 70 %}warn{% endif %}"
style="width: {{ pct }}%;"
role="progressbar" aria-valuenow="{{ pct }}" aria-valuemin="0" aria-valuemax="100"
aria-label="Array usage"></div>
</div>
<div class="table-scroll">
<table class="disk-table">
<thead>
<tr>
<th scope="col">Disk</th>
<th scope="col" class="num">Used</th>
<th scope="col" class="num">Size</th>
<th scope="col" class="num">Free</th>
<th scope="col" class="usage">Usage</th>
<th scope="col" class="num">Temp</th>
</tr>
</thead>
<tbody>
{% for dk in unraid.disks + unraid.caches %}
{% set dpct = dk.used_pct %}
<tr>
<th scope="row" title="{{ dk.device }}">{{ dk.name }}</th>
<td class="num">{{ dk.used }}</td>
<td class="num">{{ dk.total }}</td>
<td class="num">{{ dk.free }}</td>
<td class="usage">
<div class="usage-cell">
<div class="meter">
<div class="meter-fill {% if dpct > 85 %}danger{% elif dpct > 70 %}warn{% endif %}"
style="width: {{ dpct }}%;"
role="progressbar" aria-valuenow="{{ dpct }}" aria-valuemin="0" aria-valuemax="100"
aria-label="{{ dk.name }} usage"></div>
</div>
<span class="usage-pct">{{ dpct }}%</span>
</div>
</td>
<td class="num">{{ disk_state(dk) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
{# Fallback: container-visible disk usage, no per-disk detail. #}
{% set pct = unraid.array_used_pct %}
<div class="hero-figure"
style="color: {% if pct > 85 %}var(--danger){% elif pct > 70 %}var(--warn){% else %}var(--text){% endif %};">
{{ pct }}%
</div>
<div class="hero-sub">{{ unraid.array_free }} free of {{ unraid.array_total }}</div>
<div class="meter">
<div class="meter-fill {% if pct > 85 %}danger{% elif pct > 70 %}warn{% endif %}"
style="width: {{ pct }}%;"
role="progressbar" aria-valuenow="{{ pct }}" aria-valuemin="0" aria-valuemax="100"
aria-label="Array usage"></div>
</div>
<div class="meter-legend"><span>{{ unraid.array_used }} used</span></div>
{% if unraid.source == "local" %}
<p class="empty">Container disk usage ({{ unraid.mount }}). Set <code>UNRAID_API_KEY</code> for full array stats.</p>
{% endif %}
{% endif %}
{% else %}
{{ service_error("Unraid", unraid.error) }}
{% endif %}
</div>
</div>
</div>
</section>
<!-- ══ Library ══ -->
<section class="section" aria-labelledby="h-lib">
<h2 id="h-lib">Library</h2>
<div class="row cols-2">
<!-- Sonarr -->
<div class="card">
<div class="card-header">
<h3><a href="{{ service_urls.sonarr }}" target="_blank" rel="noopener">TV — Sonarr</a></h3>
</div>
<div class="card-body">
<div class="search-bar">
<label class="visually-hidden" for="sonarr-search-input">Search for a TV show</label>
<input type="text" id="sonarr-search-input" placeholder="Search and add a TV show…"
onkeydown="if(event.key==='Enter') searchSonarr()">
<button class="btn" id="sonarr-search-btn" onclick="searchSonarr()">Search</button>
</div>
<div id="sonarr-search-result" class="search-result">
<div class="search-result-info">
<div class="search-result-title" id="sonarr-result-title"></div>
<div class="search-result-meta" id="sonarr-result-meta"></div>
</div>
<button class="btn-add" id="sonarr-add-btn" onclick="confirmAddSonarr()">+ Add</button>
</div>
<div id="sonarr-search-msg" class="search-msg" style="display:none;" role="status"></div>
{% if sonarr.status == 'ok' %}
<dl class="stats">
<div><dt>Series</dt><dd>{{ sonarr.total_series }}</dd></div>
<div><dt>Missing episodes</dt><dd class="{{ 'is-warn' if sonarr.missing_episodes else '' }}">{{ sonarr.missing_episodes }}</dd></div>
</dl>
{% else %}
{{ service_error("Sonarr", sonarr.error) }}
{% endif %}
</div>
</div>
<!-- Radarr -->
<div class="card">
<div class="card-header">
<h3><a href="{{ service_urls.radarr }}" target="_blank" rel="noopener">Movies — Radarr</a></h3>
</div>
<div class="card-body">
<div class="search-bar">
<label class="visually-hidden" for="radarr-search-input">Search for a movie</label>
<input type="text" id="radarr-search-input" placeholder="Search and add a movie…"
onkeydown="if(event.key==='Enter') searchRadarr()">
<button class="btn" id="radarr-search-btn" onclick="searchRadarr()">Search</button>
</div>
<div id="radarr-search-result" class="search-result">
<div class="search-result-info">
<div class="search-result-title" id="radarr-result-title"></div>
<div class="search-result-meta" id="radarr-result-meta"></div>
</div>
<button class="btn-add" id="radarr-add-btn" onclick="confirmAddRadarr()">+ Add</button>
</div>
<div id="radarr-search-msg" class="search-msg" style="display:none;" role="status"></div>
{% if radarr.status == 'ok' %}
<dl class="stats">
<div><dt>Movies</dt><dd>{{ radarr.total_movies }}</dd></div>
<div><dt>Missing</dt><dd class="{{ 'is-warn' if radarr.missing_movies else '' }}">{{ radarr.missing_movies }}</dd></div>
</dl>
{% else %}
{{ service_error("Radarr", radarr.error) }}
{% endif %}
</div>
</div>
</div>
</section>
<!-- ══ Discover ══ -->
<section class="section" aria-labelledby="h-disc">
<h2 id="h-disc">Discover — not in your library yet</h2>
<div class="row cols-2">
<!-- Popular TV -->
<div class="card">
<div class="card-header"><h3>Popular TV on Rotten Tomatoes</h3></div>
<div class="card-body">
{% if rt_tv and rt_tv[0] is defined and rt_tv[0].error is defined %}
<p class="empty">Couldn't load TV recommendations.</p>
{% elif rt_tv %}
<div class="rt-list" id="rt-tv-list">
{% for show in rt_tv %}
<div class="rt-item {{ 'is-hidden' if loop.index > 5 }}" data-rt-title="{{ show.title }}">
<span class="rt-rank">{{ loop.index }}</span>
{% if show.poster %}<img class="rt-poster" src="{{ show.poster }}" alt="" loading="lazy">
{% else %}<span class="rt-poster"></span>{% endif %}
<div class="rt-info">
<div class="rt-title">{{ show.title }}{% if show.year %} <span class="rt-year">({{ show.year }})</span>{% endif %}</div>
<div class="rt-meta">
{% if show.certified %}<span class="score certified">Certified Fresh</span>
{% elif show.critics_score %}<span class="score critics">{{ show.critics_score }}% critics</span>{% endif %}
{% if show.audience_score %}<span class="score audience">{{ show.audience_score }} audience</span>{% endif %}
</div>
<p class="rt-desc">{{ show.description[:180] ~ '…' if show.description and show.description|length > 180 else (show.description or '') }}</p>
</div>
<button class="btn-add" data-title="{{ show.title }}" data-target="sonarr" onclick="armAdd(this)">+ Add</button>
</div>
{% endfor %}
</div>
{% if rt_tv | length > 5 %}
<button class="btn btn-quiet show-more" onclick="toggleMore(this, 'rt-tv-list')"
aria-expanded="false">Show {{ rt_tv | length - 5 }} more</button>
{% endif %}
{% else %}
<p class="empty">No TV recommendations available.</p>
{% endif %}
</div>
</div>
<!-- Top box office -->
<div class="card">
<div class="card-header"><h3>Top box office on Rotten Tomatoes</h3></div>
<div class="card-body">
{% if rt_movies and rt_movies[0] is defined and rt_movies[0].error is defined %}
<p class="empty">Couldn't load movie recommendations.</p>
{% elif rt_movies %}
<div class="rt-list" id="rt-movie-list">
{% for movie in rt_movies %}
<div class="rt-item {{ 'is-hidden' if loop.index > 5 }}" data-rt-title="{{ movie.title }}">
<span class="rt-rank">{{ loop.index }}</span>
{% if movie.poster %}<img class="rt-poster" src="{{ movie.poster }}" alt="" loading="lazy">
{% else %}<span class="rt-poster"></span>{% endif %}
<div class="rt-info">
<div class="rt-title">{{ movie.title }}{% if movie.year %} <span class="rt-year">({{ movie.year }})</span>{% endif %}</div>
<div class="rt-meta">
{% if movie.certified %}<span class="score certified">Certified Fresh</span>
{% elif movie.critics_score %}<span class="score critics">{{ movie.critics_score }}% critics</span>{% endif %}
{% if movie.audience_score %}<span class="score audience">{{ movie.audience_score }} audience</span>{% endif %}
</div>
<p class="rt-desc">{{ movie.description[:180] ~ '…' if movie.description and movie.description|length > 180 else (movie.description or '') }}</p>
</div>
<button class="btn-add" data-title="{{ movie.title }}" data-target="radarr" onclick="armAdd(this)">+ Add</button>
</div>
{% endfor %}
</div>
{% if rt_movies | length > 5 %}
<button class="btn btn-quiet show-more" onclick="toggleMore(this, 'rt-movie-list')"
aria-expanded="false">Show {{ rt_movies | length - 5 }} more</button>
{% endif %}
{% else %}
<p class="empty">No movie recommendations available.</p>
{% endif %}
</div>
</div>
</div>
</section>
<!-- ══ Requests + Indexers ══ -->
<section class="section" aria-labelledby="h-ops">
<h2 id="h-ops">Requests &amp; indexers</h2>
<div class="row cols-2 align-start">
<!-- Ombi -->
<div class="card">
<div class="card-header">
<h3><a href="{{ service_urls.ombi }}" target="_blank" rel="noopener">Requests — Ombi</a></h3>
{% if ombi.status == 'ok' %}
<span class="card-meta">{{ ombi.total_pending }} pending of {{ ombi.total_requests }}</span>
{% endif %}
</div>
<div class="card-body">
{% if ombi.status == 'ok' %}
{% if ombi.recent %}
<div class="req-list" id="req-list">
{% for req in ombi.recent %}
{# Same anatomy as an indexer row: status dot, name over a
dimmed detail line, status badge on the right. #}
{% set st = req.status | lower %}
<div class="req-item {{ 'is-hidden' if loop.index > 5 }}">
<span class="status-light {{ 'up' if st in ('available', 'approved')
else ('down' if st == 'denied' else 'warn') }}"></span>
<div class="req-info">
<div class="req-title">{{ req.title }}</div>
<div class="req-sub"><span class="req-type">{{ req.type }}</span> · {{ req.user }} · {{ req.date_display }}</div>
</div>
<span class="badge {{ st }}">{{ req.status }}</span>
</div>
{% endfor %}
</div>
{% if ombi.recent | length > 5 %}
<button class="btn btn-quiet show-more show-more-row" onclick="toggleMore(this, 'req-list')"
aria-expanded="false">Show {{ ombi.recent | length - 5 }} more</button>
{% endif %}
{% else %}
<p class="empty">No recent requests.</p>
{% endif %}
{% else %}
{{ service_error("Ombi", ombi.error) }}
{% endif %}
</div>
</div>
<!-- Prowlarr -->
<div class="card" id="prowlarr-card">
<div class="card-header">
<h3><a href="{{ service_urls.prowlarr }}" target="_blank" rel="noopener">Indexers — Prowlarr</a></h3>
{% if prowlarr.status == 'ok' %}
<span class="card-meta">
<span id="prowlarr-up-count">{{ prowlarr.up_count }}</span>/<span id="prowlarr-total">{{ prowlarr.total }}</span> online
{%- if prowlarr.has_stats %} · share of {{ prowlarr.share_metric }}, {{ prowlarr.stats_days }}d{% endif %}
</span>
{% endif %}
</div>
<div class="card-body" id="prowlarr-body">
{% if prowlarr.status == 'ok' %}
{# Bars are scaled against the busiest indexer, not against 100%,
so a spread of single-digit shares is still readable. The label
beside each bar stays the true percentage. #}
{% set top_share = prowlarr.indexers[0].share if prowlarr.indexers else 0 %}
<div class="indexer-grid" id="indexer-grid">
{% for idx in prowlarr.indexers %}
{% set share = idx.share %}
<div class="indexer-item {{ 'up' if idx.up else 'down' }}" id="idx-{{ idx.id }}">
<span class="status-light {{ 'up' if idx.up else 'down' }}" id="idx-light-{{ idx.id }}"></span>
<div class="indexer-info">
<div class="indexer-name" title="{{ idx.name }}">{{ idx.name }}</div>
<div class="indexer-sub">
<span class="proto">{{ idx.protocol }}</span>{% if prowlarr.has_stats %} · {{ '{:,}'.format(idx.grabs) }} grabs · {{ '{:,}'.format(idx.queries) }} queries{% endif %}
</div>
{% if prowlarr.has_stats %}
<div class="share-bar" role="img"
aria-label="{{ share }}% of {{ prowlarr.share_metric }} in the last {{ prowlarr.stats_days }} days">
<span style="width: {{ (share / top_share * 100) | round(1) if top_share else 0 }}%"></span>
</div>
{% endif %}
</div>
{% if prowlarr.has_stats %}
<span class="share-pct">{{ share }}%</span>
{% endif %}
<span class="badge {{ 'online' if idx.up else 'offline' }}"
id="idx-badge-{{ idx.id }}">{{ 'Online' if idx.up else 'Offline' }}</span>
</div>
{% endfor %}
</div>
{% else %}
{{ service_error("Prowlarr", prowlarr.error) }}
{% endif %}
</div>
</div>
</div>
</section>
<!-- ══ Insights ══ -->
{% if tautulli.status == 'ok' %}
<section class="section" aria-labelledby="h-ins">
<h2 id="h-ins">Watch insights — last 30 days</h2>
<div class="row cols-2">
<div class="card">
<div class="card-header"><h3>Most played</h3></div>
<div class="card-body">
{% if tautulli.top_played %}
<div class="top-list">
{% for item in tautulli.top_played %}
<div class="top-item">
<span class="top-rank">{{ loop.index }}</span>
<div class="top-info">
<div class="top-name">{{ item.title }}</div>
<div class="top-sub">{{ item.user }}</div>
</div>
{% set mt = item.media_type | lower %}
<span class="badge {{ 'tv' if mt == 'episode' else 'movie' }}">{{ 'TV' if mt == 'episode' else ('Movie' if mt == 'movie' else mt or '?') }}</span>
<span class="top-plays">{{ item.plays }}×</span>
</div>
{% endfor %}
</div>
{% else %}
<p class="empty">No play history yet.</p>
{% endif %}
</div>
</div>
<div class="card">
<div class="card-header"><h3>Top viewers</h3></div>
<div class="card-body">
{% if tautulli.top_users %}
<div class="top-list">
{% for u in tautulli.top_users %}
<div class="top-item">
<span class="top-rank">{{ loop.index }}</span>
<div class="top-info">
<div class="top-name">{{ u.username }}</div>
</div>
<span class="top-plays">{{ u.plays }} plays</span>
</div>
{% endfor %}
</div>
{% else %}
<p class="empty">No viewer data yet.</p>
{% endif %}
</div>
</div>
</div>
{% if tautulli.top_tv_list or tautulli.top_movie_list %}
<div class="row cols-2">
<div class="card">
<div class="card-header"><h3>Top TV</h3></div>
<div class="card-body">
{% if tautulli.top_tv_list %}
<div class="top-list">
{% for item in tautulli.top_tv_list %}
<div class="top-item">
<span class="top-rank">{{ loop.index }}</span>
<div class="top-info"><div class="top-name">{{ item.title }}</div></div>
<span class="top-plays">{{ item.plays }} plays</span>
</div>
{% endfor %}
</div>
{% else %}<p class="empty">No data yet.</p>{% endif %}
</div>
</div>
<div class="card">
<div class="card-header"><h3>Top movies</h3></div>
<div class="card-body">
{% if tautulli.top_movie_list %}
<div class="top-list">
{% for item in tautulli.top_movie_list %}
<div class="top-item">
<span class="top-rank">{{ loop.index }}</span>
<div class="top-info"><div class="top-name">{{ item.title }}</div></div>
<span class="top-plays">{{ item.plays }} plays</span>
</div>
{% endfor %}
</div>
{% else %}<p class="empty">No data yet.</p>{% endif %}
</div>
</div>
</div>
{% endif %}
</section>
{% endif %}
</main>
<footer>
Media Centre
<span class="version">v{{ app_version }} · {{ app_version_date }}</span>
</footer>
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
</body>
</html>