Escape what the arrs report about themselves, and give the tab an assistant
Health messages go straight from each arr's API into innerHTML and quote things nobody here controls — indexer names from Prowlarr, import-list names, release titles. The page used no escaping at all. _sz() was also binary while labelling its output TB, so /tv read 86.9 TB here and 95.6 TB on every other tab; it has one caller and that caller is disk capacity, so it now matches the rest.
This commit is contained in:
@@ -23,12 +23,21 @@ function _fmtBytes(b) {
|
||||
return (b / 1024).toFixed(0) + ' KB';
|
||||
}
|
||||
|
||||
// Bytes → auto-scale TB / GB / MB
|
||||
// Bytes → auto-scale TB / GB / MB, decimal.
|
||||
//
|
||||
// Its only caller is the arr disk bar, which is showing the same storage the Monitor and Watchdog
|
||||
// tabs show. Those were moved to decimal so a disk reads the same number here as on Unraid's own
|
||||
// Main page and as printed on the drive; leaving this one binary made /tv 86.9 TB on this tab and
|
||||
// 95.6 TB on the next, which is worse than either convention chosen consistently.
|
||||
//
|
||||
// Capacity is decimal, memory stays binary — 128 GB of RAM genuinely is 125.8 GiB and every tool
|
||||
// on the box agrees. See _vv_api_bytes_to_gib() in include/unraid_api.php for the same split on
|
||||
// the PHP side.
|
||||
function _sz(b) {
|
||||
if (!b) return '—';
|
||||
if (b >= _TB) return (b / _TB).toFixed(1) + ' TB';
|
||||
if (b >= _GB) return (b / _GB).toFixed(1) + ' GB';
|
||||
return (b / _MB).toFixed(0) + ' MB';
|
||||
if (b >= 1e12) return (b / 1e12).toFixed(1) + ' TB';
|
||||
if (b >= 1e9) return (b / 1e9).toFixed(1) + ' GB';
|
||||
return (b / 1e6).toFixed(0) + ' MB';
|
||||
}
|
||||
|
||||
function _gb(b) { return !b ? '—' : (b / _GB).toFixed(1) + ' GB'; }
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
// api/arrs.php polled every 60s → include/arrs.php
|
||||
// api/confform.php inline conf edits → include/confform.php
|
||||
require_once dirname(__DIR__) . '/include/confui.php';
|
||||
require_once dirname(__DIR__) . '/include/ai_chat.php';
|
||||
?>
|
||||
<style>
|
||||
/* ── Cards ───────────────────────────────────────────────── */
|
||||
@@ -116,6 +117,28 @@ require_once dirname(__DIR__) . '/include/confui.php';
|
||||
</div>
|
||||
|
||||
|
||||
<?php if (vv_ai_ui_on()): ?>
|
||||
<div class="vv-card" id="vv-arr-ai-card" style="margin-top:12px;">
|
||||
<?php
|
||||
// The factory, the profile registry and the store are three separate emits and none implies
|
||||
// the others — omitting any renders a chat that looks complete and dies on the first click.
|
||||
vv_ai_profiles_script();
|
||||
vv_ai_chat_store_script();
|
||||
vv_ai_chat_assets();
|
||||
// The health messages on this page quote indexers and lists by name and link to servarr's wiki,
|
||||
// which is a lot of vocabulary to carry to a search box. Scoped to the tab so "why are the lists
|
||||
// unavailable" resolves against what is on screen.
|
||||
vv_ai_chat_markup('vv-arr-ai', [
|
||||
'profile' => 'varaverk',
|
||||
'compact' => true,
|
||||
'title' => 'Assistant',
|
||||
'scopeLabel' => 'Arrs',
|
||||
'empty' => 'Ask about an instance, a health warning, or what the cleanups did.',
|
||||
'placeholder' => 'Ask about what is on this page…',
|
||||
]); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
// The conf sections this page is about, drawn by the shared renderer. They were reachable
|
||||
// only from the Settings tab's catch-all, which is a long way to go for a setting named
|
||||
@@ -141,7 +164,7 @@ function _arrCard(arr) {
|
||||
<span><span class="vv-arr-dot" style="background:${dotCol}"></span>
|
||||
<span class="vv-arr-ver">${msg}</span></span>
|
||||
</div>
|
||||
${arr.root ? `<div class="vv-arr-root">${arr.root}</div>` : ''}
|
||||
${arr.root ? `<div class="vv-arr-root">${vvEscHtml(arr.root)}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -204,7 +227,13 @@ function _arrCard(arr) {
|
||||
if (arr.health?.length) {
|
||||
const hasErr = arr.health.some(h => h.type === 'error');
|
||||
const cls = hasErr ? 'vv-arr-health' : 'vv-arr-health warn';
|
||||
const msgs = arr.health.slice(0,3).map(h => h.message || h.type).join('<br>');
|
||||
// Escaped, and it is the one string on this page that most needs it. These messages come
|
||||
// straight from the arr's own API and quote things nobody here controls — indexer names from
|
||||
// Prowlarr, import-list names, release titles. "Indexers unavailable due to failures: NzbNoob
|
||||
// (Prowlarr)" is an attacker-supplied substring wearing a status message's clothes, and it
|
||||
// was going into innerHTML raw. The <br> is joined after escaping so the separator survives
|
||||
// while the content cannot introduce markup of its own.
|
||||
const msgs = arr.health.slice(0,3).map(h => vvEscHtml(h.message || h.type)).join('<br>');
|
||||
health = `<div class="${cls}">${msgs}</div>`;
|
||||
}
|
||||
|
||||
@@ -243,7 +272,7 @@ function _arrCard(arr) {
|
||||
<span class="vv-arr-name">${name}</span>
|
||||
<span>
|
||||
<span class="vv-arr-dot" style="background:${dotCol}"></span>
|
||||
<span class="vv-arr-ver">${arr.version || 'online'}</span>
|
||||
<span class="vv-arr-ver">${vvEscHtml(arr.version || 'online')}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="vv-arr-stats">${stats}</div>
|
||||
@@ -252,7 +281,7 @@ function _arrCard(arr) {
|
||||
<div class="vv-arr-q">${qHtml}</div>
|
||||
${health}
|
||||
${hasMeta ? `<hr class="vv-arr-sep" style="margin:8px 0 4px;">${cleanup}${disc}` : ''}
|
||||
<div class="vv-arr-root">${arr.root || ''}</div>
|
||||
<div class="vv-arr-root">${vvEscHtml(arr.root || '')}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -268,7 +297,7 @@ function _nodeSection(node) {
|
||||
body = `<div class="vv-arr-cards">${node.arrs.map(_arrCard).join('')}</div>`;
|
||||
} else if (isMiss) {
|
||||
tag = `<span class="vv-arr-node-tag" style="background:#111;border-color:#222;color:#333;">no data</span>
|
||||
<button id="vv-arr-rfsh-${node.host}" onclick="vvArrsRefreshRemote('${node.host}')"
|
||||
<button id="vv-arr-rfsh-${vvEscAttr(node.host)}" onclick="vvArrsRefreshRemote('${vvEscAttr(node.host)}')"
|
||||
style="font-size:9px;color:#4a9eff;background:#0a1a2a;border:1px solid #1a3a5a;
|
||||
border-radius:3px;padding:1px 8px;cursor:pointer;margin-left:6px;">↻ Fetch now</button>`;
|
||||
body = `<div style="color:#333;font-size:11px;padding:10px 0;">
|
||||
@@ -280,7 +309,7 @@ function _nodeSection(node) {
|
||||
: age < 86400 ? Math.floor(age/3600) + 'h ago'
|
||||
: Math.floor(age/86400) + 'd ago';
|
||||
tag = `<span class="vv-arr-node-tag" style="background:#1a1000;border-color:#3a2800;color:#b87;">cached ${ageStr}</span>
|
||||
<button id="vv-arr-rfsh-${node.host}" onclick="vvArrsRefreshRemote('${node.host}')"
|
||||
<button id="vv-arr-rfsh-${vvEscAttr(node.host)}" onclick="vvArrsRefreshRemote('${vvEscAttr(node.host)}')"
|
||||
style="font-size:9px;color:#666;background:none;border:1px solid #2a2a2a;
|
||||
border-radius:3px;padding:1px 8px;cursor:pointer;margin-left:6px;">↻</button>`;
|
||||
body = `<div class="vv-arr-cards" style="opacity:.85;">${node.arrs.map(_arrCard).join('')}</div>`;
|
||||
@@ -288,8 +317,8 @@ function _nodeSection(node) {
|
||||
|
||||
return `<div class="vv-arr-node">
|
||||
<div class="vv-arr-node-hdr">
|
||||
<span class="vv-arr-node-id">${node.host.toUpperCase()}</span>
|
||||
<span class="vv-arr-node-host">${node.name}</span>${tag}
|
||||
<span class="vv-arr-node-id">${vvEscHtml(String(node.host || '').toUpperCase())}</span>
|
||||
<span class="vv-arr-node-host">${vvEscHtml(node.name || '')}</span>${tag}
|
||||
</div>
|
||||
${body}
|
||||
</div>`;
|
||||
@@ -484,4 +513,16 @@ function vvArrSaveAge() {
|
||||
})
|
||||
.catch(() => { btn.disabled = false; btn.textContent = 'Save'; fb.style.color='#ef5350'; fb.textContent='Failed'; });
|
||||
}
|
||||
|
||||
// ── Assistant ────────────────────────────────────────────────────────────────
|
||||
if (document.getElementById('vv-arr-ai-chat')) {
|
||||
VvAiChat({
|
||||
prefix: 'vv-arr-ai',
|
||||
profile: 'varaverk',
|
||||
scopeLabel: 'Arrs',
|
||||
scope: () => 'Arrs',
|
||||
resumeProfile: 'varaverk',
|
||||
empty: 'Ask about an instance, a health warning, or what the cleanups did.',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user