All checks were successful
Deploy / deploy (push) Successful in 39s
Sonarr has no concept of a requester, so this cross-references Ombi's TV request list by title to attribute the show to whoever's request triggered the grab, falling back to "Requester unknown" for anything added straight in Sonarr or when Ombi can't be reached. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPJxtE9tPURsaWDZ6rC4Dt
203 lines
10 KiB
HTML
203 lines
10 KiB
HTML
{% extends "_shell.html" %}
|
|
{% from "_macros.html" import service_error, bar, disk_state, disk_row, trend %}
|
|
{% block title %}P105 System{% endblock %}
|
|
|
|
{% block content %}
|
|
<h1 class="tt-dh bloom tt-masthead">SYSTEM</h1>
|
|
|
|
{#- ── Service transmission status ───────────────────────────────
|
|
Six services, one row each, colour carrying up or down. This is
|
|
what the old header pills were, given room to say something. Each
|
|
row opens that service's own instance, via the reverse proxy. -#}
|
|
<div class="tt-field"><span>Transmission</span></div>
|
|
<div class="tt-rows">
|
|
{% for name, key, svc in [('Sonarr', 'sonarr', sonarr), ('Radarr', 'radarr', radarr), ('SABnzbd', 'sabnzbd', sabnzbd),
|
|
('Tautulli', 'tautulli', tautulli), ('Prowlarr', 'prowlarr', prowlarr), ('Ombi', 'ombi', ombi)] %}
|
|
<a class="tt-row" href="{{ service_urls[key] }}" target="_blank" rel="noopener">
|
|
<span class="tt-row-name">{{ name }} <span class="tt-row-ext" aria-hidden="true">↗</span></span>
|
|
<span class="tt-row-body"></span>
|
|
<span class="tt-row-val {{ 'ok' if svc.status == 'ok' else 'alert' }}">
|
|
{{- 'ON AIR' if svc.status == 'ok' else 'OFF AIR' -}}
|
|
</span>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
{#- ── Array ───────────────────────────────────────────────────── -#}
|
|
{% if unraid.status != 'ok' %}
|
|
<div class="tt-field crit"><span>Array</span><span class="tt-field-meta">unreachable</span></div>
|
|
{{ service_error('Unraid', unraid.error) }}
|
|
{% else %}
|
|
{%- set apct = unraid.array_used_pct | default(0) | float -%}
|
|
<div class="tt-field {% if apct >= 90 %}crit{% elif apct >= 75 %}warn{% endif %}">
|
|
<span>Array</span>
|
|
<span class="tt-field-meta">
|
|
{{ unraid.array_state | default('') }}
|
|
{%- if unraid.parities %} · parity
|
|
{%- for p in unraid.parities %} {{ p.temp if p.temp is not none else '—' }}°C{% endfor %}
|
|
{%- endif %}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="tt-figs">
|
|
<div class="tt-fig">
|
|
<div class="tt-dh tt-fig-v {{ 'alert' if apct >= 90 else ('warn' if apct >= 75 else 'ok') }}">{{ apct | round | int }}%</div>
|
|
<div class="tt-fig-l">Used</div>
|
|
</div>
|
|
<div class="tt-fig">
|
|
<div class="tt-dh tt-fig-v">{{ unraid.array_free | default('—') }}</div>
|
|
<div class="tt-fig-l">Free</div>
|
|
</div>
|
|
<div class="tt-fig">
|
|
<div class="tt-dh tt-fig-v">{{ unraid.array_total | default('—') }}</div>
|
|
<div class="tt-fig-l">Total</div>
|
|
</div>
|
|
<div class="tt-fig">
|
|
<div class="tt-dh tt-fig-v">{{ unraid.disk_count | default(0) }}</div>
|
|
<div class="tt-fig-l">Disks</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if unraid.disks %}
|
|
{#- Desktop: a real table — columns line up, a new metric is a new
|
|
column. Hidden below 560px, where six columns stop fitting and
|
|
a horizontal scroll would hide the bar and the temperature,
|
|
the two readings that matter most on this page. -#}
|
|
<div class="tt-table-scroll">
|
|
<table class="tt-table">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Disk</th>
|
|
<th scope="col">Device</th>
|
|
<th scope="col" class="num">Size</th>
|
|
<th scope="col" class="num">Free</th>
|
|
<th scope="col">Usage</th>
|
|
<th scope="col" class="num">Temp</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for dk in unraid.disks %}
|
|
<tr>
|
|
<th scope="row">{{ dk.name }}</th>
|
|
<td>{{ dk.device | default('—') }}</td>
|
|
<td class="num">{{ dk.total | default('—') }}</td>
|
|
<td class="num">{{ dk.free | default('—') }}</td>
|
|
<td>{{ bar(dk.used_pct | default(0)) }} <span class="val">{{ dk.used_pct | default(0) | round | int }}%</span></td>
|
|
<td class="num">{{ disk_state(dk) }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% for ck in unraid.caches | default([]) %}
|
|
<tr>
|
|
<th scope="row">{{ ck.name }}</th>
|
|
<td>{{ ck.device | default('—') }}</td>
|
|
<td class="num">{{ ck.total | default('—') }}</td>
|
|
<td class="num">{{ ck.free | default('—') }}</td>
|
|
<td>{{ bar(ck.used_pct | default(0)) }} <span class="val">{{ ck.used_pct | default(0) | round | int }}%</span></td>
|
|
<td class="num">{{ disk_state(ck) }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{#- Phone (≤560px): the .tt-row-derived reflow — name, the shared
|
|
20-cell bar and the temperature on one line, device/size/free
|
|
beneath. Hidden above 560px in favour of the table. -#}
|
|
<div class="tt-disk-rows">
|
|
{% for dk in unraid.disks %}{{ disk_row(dk) }}{% endfor %}
|
|
{% for ck in unraid.caches | default([]) %}{{ disk_row(ck) }}{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
{#- ── Downloads ─────────────────────────────────────────────────
|
|
The one place the service holds real history, so the one place a
|
|
trend arrow is honest: current speed against the 7-day average. -#}
|
|
{% if sabnzbd.status == 'ok' %}
|
|
<div class="tt-field"><span>Downloads</span><span class="tt-field-meta">{{ sabnzbd.sab_status | default('') }}</span></div>
|
|
<div class="tt-figs">
|
|
<div class="tt-fig">
|
|
<div class="tt-dh tt-fig-v"><span id="sab-queue">{{ sabnzbd.queue_count }}</span></div>
|
|
<div class="tt-fig-l">In queue</div>
|
|
</div>
|
|
<div class="tt-fig">
|
|
<div class="tt-dh tt-fig-v"><span id="sab-speed">{{ sabnzbd.speed | default('0') }}</span> {{ trend(sabnzbd.speed_kbps, sabnzbd.avg_speed_kbps) }}</div>
|
|
<div class="tt-fig-l">Speed vs 7-day avg</div>
|
|
</div>
|
|
<div class="tt-fig">
|
|
<div class="tt-dh tt-fig-v"><span id="sab-remaining">{{ sabnzbd.sizeleft | default('—') }}</span></div>
|
|
<div class="tt-fig-l">Remaining</div>
|
|
</div>
|
|
<div class="tt-fig">
|
|
<div class="tt-dh tt-fig-v">{{ sabnzbd.week_size | default('—') }}</div>
|
|
<div class="tt-fig-l">This week</div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="tt-field crit"><span>Downloads</span><span class="tt-field-meta">off air</span></div>
|
|
{{ service_error('SABnzbd', sabnzbd.error) }}
|
|
{% endif %}
|
|
|
|
{#- ── Indexers ────────────────────────────────────────────────── -#}
|
|
{% if prowlarr.status == 'ok' %}
|
|
<div class="tt-field">
|
|
<span>Indexers</span>
|
|
<span class="tt-field-meta">{{ prowlarr.up_count }} of {{ prowlarr.total }} up</span>
|
|
</div>
|
|
<div class="tt-rows">
|
|
{% for idx in prowlarr.indexers %}
|
|
<div class="tt-row">
|
|
<span class="tt-row-name">{{ idx.protocol | default('') }}</span>
|
|
<span class="tt-row-body">
|
|
<span class="tt-row-title">{{ idx.name }}</span>
|
|
{% if prowlarr.has_stats %}<span class="tt-row-sub">{{ idx.grabs | default(0) }} grabs · {{ idx.queries | default(0) }} queries</span>{% endif %}
|
|
</span>
|
|
{% if prowlarr.has_stats %}{{ bar(idx.grab_share | default(0), 'ok' if idx.up else 'alert') }}{% endif %}
|
|
<span class="tt-row-val {{ 'ok' if idx.up else 'alert' }}">{{ 'UP' if idx.up else 'DOWN' }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="tt-field crit"><span>Indexers</span><span class="tt-field-meta">off air</span></div>
|
|
{{ service_error('Prowlarr', prowlarr.error) }}
|
|
{% endif %}
|
|
|
|
{#- ── Admin ─────────────────────────────────────────────────────
|
|
Shows Sonarr is holding, sized, that nobody has ever played in
|
|
Tautulli and that have sat untouched for longer than the
|
|
neglect window — largest first, since the point is reclaiming
|
|
space. Delete removes the series from Sonarr with its episode
|
|
files and folder; that's permanent, so it shares the site's
|
|
two-tap arming rather than a native confirm(). -#}
|
|
{% if admin_unwatched.status != 'ok' %}
|
|
<div class="tt-field crit"><span>Admin</span><span class="tt-field-meta">off air</span></div>
|
|
{{ service_error('Sonarr/Tautulli', admin_unwatched.error) }}
|
|
{% else %}
|
|
<div class="tt-field">
|
|
<span>Admin</span>
|
|
<span class="tt-field-meta" id="admin-meta">
|
|
{{ admin_unwatched.shows | length }} unwatched · {{ admin_unwatched.total_display }} reclaimable
|
|
</span>
|
|
</div>
|
|
{% if admin_unwatched.shows %}
|
|
<div class="tt-rows" id="admin-rows">
|
|
{% for show in admin_unwatched.shows %}
|
|
<div class="tt-row" data-bytes="{{ show.size }}">
|
|
<span class="tt-row-name">{{ show.ep_count }} EP</span>
|
|
<span class="tt-row-body">
|
|
<span class="tt-row-title">{{ show.title }}</span>
|
|
<span class="tt-row-sub">{% if show.year %}{{ show.year }} · {% endif %}Downloaded {{ show.added[:10] if show.added else 'unknown' }} · {% if show.requested_by %}Requested by {{ show.requested_by }}{% else %}Requester unknown{% endif %} · Never watched</span>
|
|
</span>
|
|
<span class="tt-row-val">{{ show.size_display }}</span>
|
|
<span class="tt-row-acts">
|
|
<button class="tt-btn tt-btn-no tt-btn-sm" data-id="{{ show.sonarr_id }}"
|
|
onclick="armDelete(this)" aria-label="Delete {{ show.title }}">Delete</button>
|
|
</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<p class="tt-empty" id="admin-empty">Nothing unwatched and older than {{ admin_unwatched.months }} months is taking up meaningful space.</p>
|
|
{% endif %}
|
|
{% endif %}
|
|
{% endblock %}
|