Fix System page crash, retire "held" copy, and slim CLAUDE.md
All checks were successful
Deploy / deploy (push) Successful in 27s
All checks were successful
Deploy / deploy (push) Successful in 27s
- system.html crashed with a 500 whenever SABnzbd history loaded: the trend() macro compared the formatted "avg_speed" string (e.g. "900 KB/s") against a number. Added a real numeric avg_speed_kbps field and pointed the macro at that instead. - Replaced "held"/"not held" wording with "in library"/"not in library" on Home and Discover. - Moved CLAUDE.md's Deployment/reverse-proxy and account-management sections into two on-demand skills (deploy-arr-summary, manage-accounts) so they only load when actually relevant, with a one-line pointer left in their place. - Updated PRODUCT.md's restructure note now that meridian-marquee has merged to main. - Dropped two stale/broken entries from settings.local.json's permission allowlist (a rule from a different machine/path, and one with a corrupted token). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBw1QodG8ZMYHrGeQfqyDS
This commit is contained in:
parent
bb3e9af056
commit
08491c0c9b
9 changed files with 153 additions and 133 deletions
|
|
@ -21,9 +21,7 @@
|
|||
"Bash(expect:*)",
|
||||
"Bash(bash export-to-unraid.sh root@192.168.86.33)",
|
||||
"Bash(scp:*)",
|
||||
"Bash(rsync:*)",
|
||||
"Bash(/Users/matt/Desktop/summary/arr-summary/gunicorn.conf.py:*)",
|
||||
"Bash(__NEW_LINE_58e4516f9721fb50__ ssh -o StrictHostKeyChecking=no -o IdentitiesOnly=yes root@192.168.86.33 \"cd /mnt/user/appdata/greendale && docker compose build --no-cache 2>&1 | tail -3 && docker compose up -d 2>&1\")"
|
||||
"Bash(rsync:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
83
.claude/skills/deploy-arr-summary/SKILL.md
Normal file
83
.claude/skills/deploy-arr-summary/SKILL.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
---
|
||||
name: deploy-arr-summary
|
||||
description: Use when deploying Arr Summary (Media Centre), restarting the arr-summary container on Unraid, confirming a deploy landed, or troubleshooting the Nginx Proxy Manager reverse proxy / TLS certs for hippofam.com. Covers the Forgejo push-to-deploy pipeline, manual container restarts, and the DNS-01/Cox port-80 constraints.
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
**Target:** Unraid server, container path `/mnt/user/appdata/greendale`
|
||||
(note the directory name differs from the container/repo name), built from
|
||||
`Dockerfile`/`docker-compose.yml` in this repo. Container name
|
||||
`arr-summary`, port `5055` (overridable via `HOST_PORT`) → container port
|
||||
`5000`.
|
||||
|
||||
**Deploy mechanism: Forgejo push-to-deploy**, same pattern as the other
|
||||
three projects on this box — see `card-grader`'s `CLAUDE.md` for the full
|
||||
pipeline mechanics. Push to `main` on the `forgejo` remote
|
||||
(`https://git.hippofam.com/ninja_hippo/arr-summary.git`) triggers
|
||||
`.forgejo/workflows/deploy.yml`, which syncs into
|
||||
`/mnt/user/appdata/greendale` (excluding `.git`, `.env`, `data/`) and runs
|
||||
`docker compose up -d --build`.
|
||||
|
||||
**Restarting the container manually: use Compose, not the Unraid Docker
|
||||
UI.** Every deploy recreates the container with a new Docker ID (same
|
||||
name, `arr-summary`). Unraid's Docker tab caches the old ID, so hitting
|
||||
Restart there after a deploy throws "No such container" — harmless, but
|
||||
confusing. From the Unraid terminal:
|
||||
```bash
|
||||
cd /mnt/user/appdata/greendale && docker compose restart
|
||||
```
|
||||
`docker compose up -d --force-recreate` for a guaranteed-clean recreate;
|
||||
`docker ps --filter name=arr-summary` to check real state regardless of
|
||||
what the Unraid UI shows.
|
||||
|
||||
**Confirming a deploy actually landed:** the dashboard footer renders
|
||||
`v<APP_VERSION> · <APP_VERSION_DATE>` from constants near the top of
|
||||
`app.py` (both env-overridable, injected into every template by the
|
||||
`inject_version` context processor). Bump them when cutting a deploy —
|
||||
loading the page is then the quickest honest check that the new build is
|
||||
live, without trusting the Unraid Docker tab's cached container IDs.
|
||||
|
||||
## Reverse proxy (Nginx Proxy Manager)
|
||||
|
||||
Two NPM proxy hosts front this app and `git.hippofam.com`, both using
|
||||
Let's Encrypt certs issued via **DNS-01 through Porkbun** — required,
|
||||
not optional: **Cox blocks inbound port 80** on this connection, so
|
||||
HTTP-01 challenges (and plain `http://` requests generally) can never
|
||||
work here. `http://hippofam.com` will always time out from outside the
|
||||
LAN; only `https://` resolves. Both proxy hosts have Force SSL on.
|
||||
|
||||
`hippofam.com` → `http://192.168.86.33:5055` (the Unraid box, this
|
||||
container's port). Verify the destination IP/port here before assuming
|
||||
an outage is DNS or app-level — it has drifted to the wrong host before.
|
||||
|
||||
On 2026-08-26 the apex host's own cert (issued Aug 2025) went dark: NPM's
|
||||
UI still showed it "In Use" with a valid, unexpired date, but nginx
|
||||
presented no certificate at all for `hippofam.com` (TLS alert 112,
|
||||
"unrecognized name") — the on-disk cert files had rotted independently
|
||||
of the DB record NPM's UI reads from. `git.hippofam.com`, on a newer
|
||||
cert from the same account, was unaffected. Fixed by pointing the proxy
|
||||
host at the `*.hippofam.com` + `hippofam.com` wildcard cert already
|
||||
issued and sitting unused, which forced NPM to regenerate the nginx
|
||||
config with known-good cert files. If `hippofam.com` ever throws a TLS
|
||||
handshake error again with NPM's UI reporting all green, suspect this
|
||||
same on-disk/DB drift before anything else — check with:
|
||||
```bash
|
||||
openssl s_client -connect <public-ip>:443 -servername hippofam.com </dev/null
|
||||
```
|
||||
A clean run shows `subject=/CN=...` and `Verify return code: 0`; the
|
||||
broken state shows `no peer certificate available`.
|
||||
|
||||
**Persistent state, never touched by deploys:** `.env` holds every API key
|
||||
and `LOGIN_PASSWORD` — this project was already doing this correctly
|
||||
before the Forgejo migration (compose file uses `${VAR}` substitution from
|
||||
`.env`, no hardcoded secrets), unlike `n64-tracker` which needed fixing.
|
||||
Runtime settings written by the Settings page (API-key overrides,
|
||||
timezone, changed login password) live in `settings.json` at
|
||||
`SETTINGS_FILE`, which the compose file points at `/data/settings.json`
|
||||
on the `./data` bind mount — i.e. `/mnt/user/appdata/greendale/data`.
|
||||
That mount is load-bearing: without it the file sits on the container
|
||||
filesystem, and every `up -d --build` recreates the container and
|
||||
silently discards it. `data/` is likewise excluded from the deploy
|
||||
rsync, which runs `--delete` and would otherwise remove it for not
|
||||
being in the repo.
|
||||
39
.claude/skills/manage-accounts/SKILL.md
Normal file
39
.claude/skills/manage-accounts/SKILL.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
name: manage-accounts
|
||||
description: Use when adding, removing, or changing dashboard login accounts for Arr Summary (Media Centre), or explaining the difference between its multi-user and legacy single-shared-password login modes.
|
||||
---
|
||||
|
||||
Two modes, chosen by whether `settings.json` holds a non-empty `users` map:
|
||||
|
||||
- **Multi-user** (once any account exists): username + password, each
|
||||
password stored as a salted pbkdf2 hash under `users` in `settings.json`.
|
||||
`verify_credentials()` is the only entry point; `load_users()` re-reads
|
||||
the file per check, so adding a user or changing a password is live
|
||||
immediately in every gunicorn worker — no restart, unlike the API keys.
|
||||
- **Legacy single shared password** (no accounts yet): the original
|
||||
`LOGIN_PASSWORD` / `settings.json.login_password` path, username ignored.
|
||||
This fallback exists so deploying multi-user support can't lock an
|
||||
existing install out; the login page hides the username field entirely
|
||||
until accounts exist.
|
||||
|
||||
Manage accounts with `manage_users.py`, which prompts for passwords
|
||||
(never echoed, never a CLI argument, so they stay out of shell history)
|
||||
and only ever writes hashes:
|
||||
|
||||
```bash
|
||||
docker exec -it arr-summary python3 manage_users.py list
|
||||
docker exec -it arr-summary python3 manage_users.py add <username>
|
||||
docker exec -it arr-summary python3 manage_users.py passwd <username>
|
||||
docker exec -it arr-summary python3 manage_users.py remove <username>
|
||||
```
|
||||
|
||||
**Adding the first account switches the whole dashboard to per-user
|
||||
logins and the shared password stops working** — verify you can sign in
|
||||
as that user before closing the shell. `remove` refuses to delete the
|
||||
last account for the same reason.
|
||||
|
||||
Users live in `settings.json` rather than an env var deliberately: a
|
||||
pbkdf2 hash contains `$`, which docker compose interpolates when reading
|
||||
`.env`. `PASSWORD_HASH_METHOD` is pinned to pbkdf2 rather than Werkzeug's
|
||||
default scrypt, which needs a hashlib built against OpenSSL and is absent
|
||||
on the macOS system Python.
|
||||
115
CLAUDE.md
115
CLAUDE.md
|
|
@ -166,119 +166,14 @@ app from a shell on an explicit `PORT` is the working path.
|
|||
|
||||
## Deployment
|
||||
|
||||
**Target:** Unraid server, container path `/mnt/user/appdata/greendale`
|
||||
(note the directory name differs from the container/repo name), built from
|
||||
`Dockerfile`/`docker-compose.yml` in this repo. Container name
|
||||
`arr-summary`, port `5055` (overridable via `HOST_PORT`) → container port
|
||||
`5000`.
|
||||
|
||||
**Deploy mechanism: Forgejo push-to-deploy**, same pattern as the other
|
||||
three projects on this box — see `card-grader`'s `CLAUDE.md` for the full
|
||||
pipeline mechanics. Push to `main` on the `forgejo` remote
|
||||
(`https://git.hippofam.com/ninja_hippo/arr-summary.git`) triggers
|
||||
`.forgejo/workflows/deploy.yml`, which syncs into
|
||||
`/mnt/user/appdata/greendale` (excluding `.git`, `.env`, `data/`) and runs
|
||||
`docker compose up -d --build`.
|
||||
|
||||
**Restarting the container manually: use Compose, not the Unraid Docker
|
||||
UI.** Every deploy recreates the container with a new Docker ID (same
|
||||
name, `arr-summary`). Unraid's Docker tab caches the old ID, so hitting
|
||||
Restart there after a deploy throws "No such container" — harmless, but
|
||||
confusing. From the Unraid terminal:
|
||||
```bash
|
||||
cd /mnt/user/appdata/greendale && docker compose restart
|
||||
```
|
||||
`docker compose up -d --force-recreate` for a guaranteed-clean recreate;
|
||||
`docker ps --filter name=arr-summary` to check real state regardless of
|
||||
what the Unraid UI shows.
|
||||
|
||||
**Confirming a deploy actually landed:** the dashboard footer renders
|
||||
`v<APP_VERSION> · <APP_VERSION_DATE>` from constants near the top of
|
||||
`app.py` (both env-overridable, injected into every template by the
|
||||
`inject_version` context processor). Bump them when cutting a deploy —
|
||||
loading the page is then the quickest honest check that the new build is
|
||||
live, without trusting the Unraid Docker tab's cached container IDs.
|
||||
|
||||
## Reverse proxy (Nginx Proxy Manager)
|
||||
|
||||
Two NPM proxy hosts front this app and `git.hippofam.com`, both using
|
||||
Let's Encrypt certs issued via **DNS-01 through Porkbun** — required,
|
||||
not optional: **Cox blocks inbound port 80** on this connection, so
|
||||
HTTP-01 challenges (and plain `http://` requests generally) can never
|
||||
work here. `http://hippofam.com` will always time out from outside the
|
||||
LAN; only `https://` resolves. Both proxy hosts have Force SSL on.
|
||||
|
||||
`hippofam.com` → `http://192.168.86.33:5055` (the Unraid box, this
|
||||
container's port). Verify the destination IP/port here before assuming
|
||||
an outage is DNS or app-level — it has drifted to the wrong host before.
|
||||
|
||||
On 2026-08-26 the apex host's own cert (issued Aug 2025) went dark: NPM's
|
||||
UI still showed it "In Use" with a valid, unexpired date, but nginx
|
||||
presented no certificate at all for `hippofam.com` (TLS alert 112,
|
||||
"unrecognized name") — the on-disk cert files had rotted independently
|
||||
of the DB record NPM's UI reads from. `git.hippofam.com`, on a newer
|
||||
cert from the same account, was unaffected. Fixed by pointing the proxy
|
||||
host at the `*.hippofam.com` + `hippofam.com` wildcard cert already
|
||||
issued and sitting unused, which forced NPM to regenerate the nginx
|
||||
config with known-good cert files. If `hippofam.com` ever throws a TLS
|
||||
handshake error again with NPM's UI reporting all green, suspect this
|
||||
same on-disk/DB drift before anything else — check with:
|
||||
```bash
|
||||
openssl s_client -connect <public-ip>:443 -servername hippofam.com </dev/null
|
||||
```
|
||||
A clean run shows `subject=/CN=...` and `Verify return code: 0`; the
|
||||
broken state shows `no peer certificate available`.
|
||||
|
||||
**Persistent state, never touched by deploys:** `.env` holds every API key
|
||||
and `LOGIN_PASSWORD` — this project was already doing this correctly
|
||||
before the Forgejo migration (compose file uses `${VAR}` substitution from
|
||||
`.env`, no hardcoded secrets), unlike `n64-tracker` which needed fixing.
|
||||
Runtime settings written by the Settings page (API-key overrides,
|
||||
timezone, changed login password) live in `settings.json` at
|
||||
`SETTINGS_FILE`, which the compose file points at `/data/settings.json`
|
||||
on the `./data` bind mount — i.e. `/mnt/user/appdata/greendale/data`.
|
||||
That mount is load-bearing: without it the file sits on the container
|
||||
filesystem, and every `up -d --build` recreates the container and
|
||||
silently discards it. `data/` is likewise excluded from the deploy
|
||||
rsync, which runs `--delete` and would otherwise remove it for not
|
||||
being in the repo.
|
||||
See the `deploy-arr-summary` skill for the Forgejo push-to-deploy
|
||||
pipeline, manual container restart steps, deploy verification, and the
|
||||
Nginx Proxy Manager / TLS setup and its Cox-port-80/DNS-01 constraint.
|
||||
|
||||
## Logins
|
||||
|
||||
Two modes, chosen by whether `settings.json` holds a non-empty `users` map:
|
||||
|
||||
- **Multi-user** (once any account exists): username + password, each
|
||||
password stored as a salted pbkdf2 hash under `users` in `settings.json`.
|
||||
`verify_credentials()` is the only entry point; `load_users()` re-reads
|
||||
the file per check, so adding a user or changing a password is live
|
||||
immediately in every gunicorn worker — no restart, unlike the API keys.
|
||||
- **Legacy single shared password** (no accounts yet): the original
|
||||
`LOGIN_PASSWORD` / `settings.json.login_password` path, username ignored.
|
||||
This fallback exists so deploying multi-user support can't lock an
|
||||
existing install out; the login page hides the username field entirely
|
||||
until accounts exist.
|
||||
|
||||
Manage accounts with `manage_users.py`, which prompts for passwords
|
||||
(never echoed, never a CLI argument, so they stay out of shell history)
|
||||
and only ever writes hashes:
|
||||
|
||||
```bash
|
||||
docker exec -it arr-summary python3 manage_users.py list
|
||||
docker exec -it arr-summary python3 manage_users.py add <username>
|
||||
docker exec -it arr-summary python3 manage_users.py passwd <username>
|
||||
docker exec -it arr-summary python3 manage_users.py remove <username>
|
||||
```
|
||||
|
||||
**Adding the first account switches the whole dashboard to per-user
|
||||
logins and the shared password stops working** — verify you can sign in
|
||||
as that user before closing the shell. `remove` refuses to delete the
|
||||
last account for the same reason.
|
||||
|
||||
Users live in `settings.json` rather than an env var deliberately: a
|
||||
pbkdf2 hash contains `$`, which docker compose interpolates when reading
|
||||
`.env`. `PASSWORD_HASH_METHOD` is pinned to pbkdf2 rather than Werkzeug's
|
||||
default scrypt, which needs a hashlib built against OpenSSL and is absent
|
||||
on the macOS system Python.
|
||||
See the `manage-accounts` skill for the multi-user vs. legacy
|
||||
single-shared-password login modes and the `manage_users.py` CLI.
|
||||
|
||||
**Never put a real password in the repo** — not in `app.py`, not in
|
||||
`settings.json` committed to git, not in a fixture. `.env` and `data/`
|
||||
|
|
|
|||
23
PRODUCT.md
23
PRODUCT.md
|
|
@ -53,16 +53,19 @@ so this is not optional). Session-based login (single shared password or
|
|||
per-user accounts) gates the whole app; there's no public/unauthenticated
|
||||
view.
|
||||
|
||||
**In-progress restructure (branch `meridian-marquee`):** the app is
|
||||
splitting from one long single-page dashboard into focused pages — Home
|
||||
("marquee": what's playing, one line of system state, what's new),
|
||||
Watch, Library, Discover, Requests, System, Activity — each answering one
|
||||
question well rather than one long scroll trying to show everything.
|
||||
This also resolves cases where a content-heavy section (e.g. the per-disk
|
||||
storage list) would otherwise force awkward row heights on a shared
|
||||
"Right now" strip. `_shell.html` and `_macros.html` are new shared-layout
|
||||
scaffolding for this split; `dashboard.html` is the outgoing single-page
|
||||
template being superseded by `home.html` + the per-page templates.
|
||||
**Multi-page restructure (shipped 2026-08-29, `meridian-marquee` merged
|
||||
to `main`):** the app split from one long single-page dashboard into
|
||||
focused pages — Home/P100, Watch, Library, Discover, Requests, System,
|
||||
Activity — each answering one question well rather than one long scroll
|
||||
trying to show everything. This also resolved cases where a content-heavy
|
||||
section (e.g. the per-disk storage list) would otherwise force awkward
|
||||
row heights on a shared "Right now" strip. `_shell.html` and
|
||||
`_macros.html` are the shared-layout scaffolding; `dashboard.html` (the
|
||||
old single-page template) was deleted, superseded by `home.html` + the
|
||||
per-page templates. Landed together with the visual redesign in
|
||||
Brand Commitments below — see DESIGN.md and
|
||||
`static/css/base.css`'s header comment for the visual system's own
|
||||
invariants.
|
||||
|
||||
Each page fetches only the services it needs (e.g. `/watch` fetches only
|
||||
Tautulli) rather than the old single route's all-services-at-once fetch —
|
||||
|
|
|
|||
8
app.py
8
app.py
|
|
@ -512,7 +512,8 @@ def _get_sab_history_cached():
|
|||
if duration > 0 and size_bytes > 0:
|
||||
speeds.append(size_bytes / duration)
|
||||
avg_bytes_per_sec = sum(speeds) / len(speeds) if speeds else 0
|
||||
avg_speed = _format_speed(avg_bytes_per_sec / 1024)
|
||||
avg_speed_kbps = avg_bytes_per_sec / 1024
|
||||
avg_speed = _format_speed(avg_speed_kbps)
|
||||
# Keep the newest completed items too — "Landed today" on the home
|
||||
# page and the activity feed both read these. Same request as before;
|
||||
# previously only the aggregates survived.
|
||||
|
|
@ -526,12 +527,12 @@ def _get_sab_history_cached():
|
|||
|
||||
result = {"week_size": week_size, "day_size": day_size,
|
||||
"completed_7d": completed_7d, "avg_speed": avg_speed,
|
||||
"recent_items": recent_items}
|
||||
"avg_speed_kbps": avg_speed_kbps, "recent_items": recent_items}
|
||||
_sab_history_cache = {"data": result, "expires": now + 600}
|
||||
return result
|
||||
except Exception:
|
||||
return {"week_size": "—", "day_size": "—", "completed_7d": "—",
|
||||
"avg_speed": "—", "recent_items": []}
|
||||
"avg_speed": "—", "avg_speed_kbps": None, "recent_items": []}
|
||||
|
||||
|
||||
def get_sabnzbd_summary():
|
||||
|
|
@ -565,6 +566,7 @@ def get_sabnzbd_summary():
|
|||
"week_size": hist["week_size"],
|
||||
"day_size": hist["day_size"],
|
||||
"avg_speed": hist["avg_speed"],
|
||||
"avg_speed_kbps": hist["avg_speed_kbps"],
|
||||
"recent_items": hist.get("recent_items", []),
|
||||
}
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@
|
|||
|
||||
{% block content %}
|
||||
<h1 class="tt-dh bloom tt-masthead">DISCOVER</h1>
|
||||
<p class="tt-empty">Popular right now, with anything already held filtered out.</p>
|
||||
<p class="tt-empty">Popular right now, with anything already in library filtered out.</p>
|
||||
|
||||
<div class="tt-field">
|
||||
<span>Films</span>
|
||||
<span class="tt-field-meta">{{ rt_movies | length }} not held</span>
|
||||
<span class="tt-field-meta">{{ rt_movies | length }} not in library</span>
|
||||
</div>
|
||||
{% if rt_movies %}
|
||||
<div class="tt-tiles">
|
||||
|
|
@ -17,12 +17,12 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="tt-empty">Nothing to suggest — the feed is unreachable or you already hold it all.</p>
|
||||
<p class="tt-empty">Nothing to suggest — the feed is unreachable or it's all already in your library.</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="tt-field">
|
||||
<span>Television</span>
|
||||
<span class="tt-field-meta">{{ rt_tv | length }} not held</span>
|
||||
<span class="tt-field-meta">{{ rt_tv | length }} not in library</span>
|
||||
</div>
|
||||
{% if rt_tv %}
|
||||
<div class="tt-tiles">
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
<a class="tt-row" href="{{ request.script_root }}/library">
|
||||
<span class="tt-row-num">102</span>
|
||||
<span class="tt-row-name">Library</span>
|
||||
<span class="tt-row-body"><span class="tt-row-sub">Everything already held</span></span>
|
||||
<span class="tt-row-body"><span class="tt-row-sub">Everything in library</span></span>
|
||||
{% if sonarr.status == 'ok' or radarr.status == 'ok' %}
|
||||
<span class="tt-row-val">{{ sonarr.total_series | default(0) }} SHOWS {{ radarr.total_movies | default(0) }} FILMS</span>
|
||||
{% else %}
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
<a class="tt-row" href="{{ request.script_root }}/discover">
|
||||
<span class="tt-row-num">103</span>
|
||||
<span class="tt-row-name">Discover</span>
|
||||
<span class="tt-row-body"><span class="tt-row-sub">Popular, and not yet held</span></span>
|
||||
<span class="tt-row-body"><span class="tt-row-sub">Popular, and not yet in library</span></span>
|
||||
<span class="tt-row-val">{{ (rt_movies | length) + (rt_tv | length) }} SUGGESTED</span>
|
||||
</a>
|
||||
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@
|
|||
<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) }}</div>
|
||||
<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">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue