|
|
|
@@ -31,6 +31,16 @@
|
|
|
|
|
// against real inventory.
|
|
|
|
|
//
|
|
|
|
|
// All remote and container-supplied strings render escaped.
|
|
|
|
|
// Through vvEscHtml()/vvEscAttr() from Varaverk.page — media titles and usernames from
|
|
|
|
|
// Emby/Jellyfin/Plex, partner hostnames and versions read over the mesh, docker folder
|
|
|
|
|
// names, VM names, UPS and GPU model strings. This line claimed to be true before any of
|
|
|
|
|
// it was: the page had no escaping at all, and the helpers it needed lived in a page it
|
|
|
|
|
// never loads.
|
|
|
|
|
//
|
|
|
|
|
// A container WebUI value is scheme-checked before it is a link.
|
|
|
|
|
// include/docker_folders.php allows only http/https out of the template XML, and
|
|
|
|
|
// vvSafeUrl() filters again at window.open(), where a javascript: URL would run with
|
|
|
|
|
// this page's origin.
|
|
|
|
|
//
|
|
|
|
|
// RENDERS
|
|
|
|
|
// System header, CPU per core, memory breakdown, GPU cards, storage pools and array disks,
|
|
|
|
@@ -477,7 +487,7 @@ function vvRenderMemory(mem) {
|
|
|
|
|
|
|
|
|
|
const procs = mem.top_procs ?? [];
|
|
|
|
|
const procStrip = procs.map(p =>
|
|
|
|
|
`<span style="white-space:nowrap;">${p.name} <span style="color:#aaa;font-weight:600;">${vvFmtGib(p.kb)}</span></span>`
|
|
|
|
|
`<span style="white-space:nowrap;">${vvEscHtml(p.name)} <span style="color:#aaa;font-weight:600;">${vvFmtGib(p.kb)}</span></span>`
|
|
|
|
|
).join('<span style="color:#333;margin:0 5px;">·</span>');
|
|
|
|
|
|
|
|
|
|
let html = `<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:10px;">
|
|
|
|
@@ -664,7 +674,7 @@ function vvDiskRow(disk) {
|
|
|
|
|
: `<span style="color:#555;font-size:10px;">${vvFmt(disk.used_gb)} / ${vvFmt(disk.size_gb)}</span>`;
|
|
|
|
|
return `<div style="margin-bottom:7px;">
|
|
|
|
|
<div style="display:flex;justify-content:space-between;align-items:center;font-size:11px;margin-bottom:3px;">
|
|
|
|
|
<span style="color:${nameColor};display:flex;align-items:center;">${disk.name}${spinLabel}${failLabel}${vvIoChip(disk.device)}</span>
|
|
|
|
|
<span style="color:${nameColor};display:flex;align-items:center;">${vvEscHtml(disk.name)}${spinLabel}${failLabel}${vvIoChip(disk.device)}</span>
|
|
|
|
|
${right}
|
|
|
|
|
<span style="color:${tempColor};font-size:10px;margin-left:6px;flex-shrink:0;">${tempStr}</span>
|
|
|
|
|
</div>
|
|
|
|
@@ -741,7 +751,7 @@ function vvPollMonitor() {
|
|
|
|
|
document.getElementById('vv-system-body').innerHTML =
|
|
|
|
|
`<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;">
|
|
|
|
|
<div style="min-width:0;">
|
|
|
|
|
<div style="font-size:14px;font-weight:700;color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${sys.name}</div>
|
|
|
|
|
<div style="font-size:14px;font-weight:700;color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${vvEscHtml(sys.name)}</div>
|
|
|
|
|
<div style="font-size:10px;color:#555;margin-top:2px;">${sys.comment || ' '}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display:flex;align-items:flex-start;gap:6px;flex-shrink:0;margin-left:6px;">
|
|
|
|
@@ -771,9 +781,9 @@ function vvPollMonitor() {
|
|
|
|
|
<div style="font-size:20px;font-weight:300;color:#ccc;line-height:1;">${timeStr}</div>
|
|
|
|
|
<div style="font-size:10px;color:#555;margin-bottom:10px;">${dateStr} · ${tz}</div>
|
|
|
|
|
<div style="display:grid;grid-template-columns:auto 1fr;gap:3px 8px;font-size:11px;">
|
|
|
|
|
<span style="color:#444;">Model</span> <span style="color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${sys.cpu_model}${_coreMeta}</span>
|
|
|
|
|
<span style="color:#444;">Array</span> <span style="color:${arrayColor};font-weight:600;">${sys.array_state}</span>
|
|
|
|
|
<span style="color:#444;">Uptime</span> <span style="color:#888;">${sys.uptime}</span>
|
|
|
|
|
<span style="color:#444;">Model</span> <span style="color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${vvEscHtml(sys.cpu_model)}${_coreMeta}</span>
|
|
|
|
|
<span style="color:#444;">Array</span> <span style="color:${arrayColor};font-weight:600;">${vvEscHtml(sys.array_state)}</span>
|
|
|
|
|
<span style="color:#444;">Uptime</span> <span style="color:#888;">${vvEscHtml(sys.uptime)}</span>
|
|
|
|
|
<span style="color:#444;">Load</span> <span style="color:${_loadColor};">${_loadStr}</span>
|
|
|
|
|
<span style="color:#444;">Running</span> <span style="color:#888;">${_runningCtrs} ctr${_runningCtrs !== 1 ? 's' : ''}${_runningVMs > 0 ? ` · ${_runningVMs} VM` : ''}</span>
|
|
|
|
|
<span style="color:#444;">Version</span> <span style="color:#3a3a3a;">${ver}</span>
|
|
|
|
@@ -812,8 +822,8 @@ function vvPollMonitor() {
|
|
|
|
|
const ramAvail = rs.mem_used_pct > 0;
|
|
|
|
|
const cpuHue = cpuAvail ? Math.round(120 * (1 - rs.cpu_load / 100)) : 0;
|
|
|
|
|
const memHue = ramAvail ? Math.round(120 * (1 - rs.mem_used_pct / 100)) : 0;
|
|
|
|
|
const cpuStr = cpuAvail ? `<span style="color:hsl(${cpuHue},70%,45%);font-weight:600;">${rs.cpu_load}%${rs.cpu_threads ? `<span style="color:#333;font-weight:400;"> · ${rs.cpu_threads}t</span>` : ''}</span>` : `<span style="color:#333;">—</span>`;
|
|
|
|
|
const ramStr = ramAvail ? `<span style="color:hsl(${memHue},70%,45%);font-weight:600;">${rs.mem_used_pct}%${rs.mem_total_gb ? `<span style="color:#333;font-weight:400;"> · ${rs.mem_total_gb}G</span>` : ''}</span>` : `<span style="color:#333;">—</span>`;
|
|
|
|
|
const cpuStr = cpuAvail ? `<span style="color:hsl(${cpuHue},70%,45%);font-weight:600;">${vvEscHtml(rs.cpu_load)}%${rs.cpu_threads ? `<span style="color:#333;font-weight:400;"> · ${vvEscHtml(rs.cpu_threads)}t</span>` : ''}</span>` : `<span style="color:#333;">—</span>`;
|
|
|
|
|
const ramStr = ramAvail ? `<span style="color:hsl(${memHue},70%,45%);font-weight:600;">${vvEscHtml(rs.mem_used_pct)}%${rs.mem_total_gb ? `<span style="color:#333;font-weight:400;"> · ${vvEscHtml(rs.mem_total_gb)}G</span>` : ''}</span>` : `<span style="color:#333;">—</span>`;
|
|
|
|
|
const arrColor = rs.array_state === 'Started' || rs.array_state === 'STARTED' ? '#4caf50' : '#f44336';
|
|
|
|
|
const uptimeStr = rs.uptime && rs.uptime !== '—' ? rs.uptime : '—';
|
|
|
|
|
|
|
|
|
@@ -823,18 +833,18 @@ function vvPollMonitor() {
|
|
|
|
|
const verMismatch = myVer && remoteVer && myVer !== remoteVer;
|
|
|
|
|
const verWarn = verMismatch
|
|
|
|
|
? `<div style="font-size:10px;color:#ff9800;margin-top:4px;padding:3px 6px;background:#1a1000;border:1px solid #3a2800;border-radius:3px;">
|
|
|
|
|
⚠ Version mismatch: local ${myVer} · remote ${remoteVer}<br>
|
|
|
|
|
⚠ Version mismatch: local ${vvEscHtml(myVer)} · remote ${vvEscHtml(remoteVer)}<br>
|
|
|
|
|
<span style="color:#555;">Script sync ops are gated until versions match</span>
|
|
|
|
|
</div>` : '';
|
|
|
|
|
|
|
|
|
|
const verRow = remoteVer ? `<span style="color:#444;">unRAID</span><span style="color:#3a3a3a;grid-column:span 3;">${remoteVer}</span>` : '';
|
|
|
|
|
const verRow = remoteVer ? `<span style="color:#444;">unRAID</span><span style="color:#3a3a3a;grid-column:span 3;">${vvEscHtml(remoteVer)}</span>` : '';
|
|
|
|
|
statsHtml = `<div style="display:grid;grid-template-columns:auto 1fr auto 1fr;gap:2px 8px;font-size:10px;margin-top:5px;margin-bottom:2px;">
|
|
|
|
|
<span style="color:#444;">CPU</span>${cpuStr}
|
|
|
|
|
<span style="color:#444;">RAM</span>${ramStr}
|
|
|
|
|
<span style="color:#444;">Array</span>
|
|
|
|
|
<span style="color:${arrColor};font-weight:600;">${rs.array_state}</span>
|
|
|
|
|
<span style="color:${arrColor};font-weight:600;">${vvEscHtml(rs.array_state)}</span>
|
|
|
|
|
<span style="color:#444;">Uptime</span>
|
|
|
|
|
<span style="color:#555;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${uptimeStr}</span>
|
|
|
|
|
<span style="color:#555;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${vvEscHtml(uptimeStr)}</span>
|
|
|
|
|
${verRow}
|
|
|
|
|
</div>${verWarn}`;
|
|
|
|
|
} else if (rs && rs.no_api_key) {
|
|
|
|
@@ -846,9 +856,9 @@ function vvPollMonitor() {
|
|
|
|
|
ptHtml += `<div style="margin-bottom:8px;padding-bottom:8px;border-bottom:1px solid #222;">
|
|
|
|
|
<div style="display:flex;align-items:center;justify-content:space-between;">
|
|
|
|
|
<div>
|
|
|
|
|
<span style="font-size:10px;color:#555;margin-right:4px;">${h.id}</span>
|
|
|
|
|
<span style="font-size:12px;color:#ccc;font-weight:500;">${h.owner || h.hostname}</span>${tags}
|
|
|
|
|
<div style="font-size:10px;color:#555;margin-top:1px;">${h.hostname}</div>
|
|
|
|
|
<span style="font-size:10px;color:#555;margin-right:4px;">${vvEscHtml(h.id)}</span>
|
|
|
|
|
<span style="font-size:12px;color:#ccc;font-weight:500;">${vvEscHtml(h.owner || h.hostname)}</span>${tags}
|
|
|
|
|
<div style="font-size:10px;color:#555;margin-top:1px;">${vvEscHtml(h.hostname)}</div>
|
|
|
|
|
${onboardBadge}
|
|
|
|
|
</div>
|
|
|
|
|
<span style="color:${dot};font-size:10px;white-space:nowrap;">● ${label}</span>
|
|
|
|
@@ -952,13 +962,13 @@ function vvPollMonitor() {
|
|
|
|
|
if (fbActive.length) {
|
|
|
|
|
fbActive.forEach(group => {
|
|
|
|
|
fbHtml += `<div style="font-size:10px;color:#555;margin-bottom:4px;letter-spacing:.03em;">
|
|
|
|
|
COVERING ${group.hostname}
|
|
|
|
|
COVERING ${vvEscHtml(group.hostname)}
|
|
|
|
|
</div>`;
|
|
|
|
|
group.containers.forEach(c => {
|
|
|
|
|
const img = c.image.includes('/') ? c.image.split('/').pop() : c.image;
|
|
|
|
|
fbHtml += `<div style="display:flex;justify-content:space-between;align-items:center;
|
|
|
|
|
background:#1a1a1a;border-radius:4px;padding:4px 8px;margin-bottom:3px;">
|
|
|
|
|
<span style="color:#ccc;font-size:11px;font-weight:500;">${c.name}</span>
|
|
|
|
|
<span style="color:#ccc;font-size:11px;font-weight:500;">${vvEscHtml(c.name)}</span>
|
|
|
|
|
<span style="color:#444;font-size:9px;margin-left:8px;white-space:nowrap;">${img}</span>
|
|
|
|
|
</div>`;
|
|
|
|
|
});
|
|
|
|
@@ -1022,8 +1032,8 @@ function vvPollMonitor() {
|
|
|
|
|
|
|
|
|
|
document.getElementById('vv-ups-body').innerHTML =
|
|
|
|
|
`<div class="vv-banner ${statCls}" style="margin-bottom:8px;">
|
|
|
|
|
<span>${ups.status}${onBatt ? ' — ON BATTERY' : ''}</span>
|
|
|
|
|
<span style="font-size:11px;font-weight:400;opacity:0.8;">${ups.model}</span>
|
|
|
|
|
<span>${vvEscHtml(ups.status)}${onBatt ? ' — ON BATTERY' : ''}</span>
|
|
|
|
|
<span style="font-size:11px;font-weight:400;opacity:0.8;">${vvEscHtml(ups.model)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;">
|
|
|
|
|
<div>
|
|
|
|
@@ -1293,7 +1303,7 @@ function vvPollMonitor() {
|
|
|
|
|
const diff = now - r.ts;
|
|
|
|
|
const ago = diff < 3600 ? Math.floor(diff / 60) + 'm' : Math.floor(diff / 3600) + 'h';
|
|
|
|
|
html += `<div style="display:flex;justify-content:space-between;font-size:10px;color:#666;margin-bottom:2px;">
|
|
|
|
|
<span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">${r.name}</span>
|
|
|
|
|
<span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">${vvEscHtml(r.name)}</span>
|
|
|
|
|
<span style="flex-shrink:0;margin-left:6px;color:#444;">${ago}</span>
|
|
|
|
|
</div>`;
|
|
|
|
|
});
|
|
|
|
@@ -1494,7 +1504,7 @@ function vvPollMonitor() {
|
|
|
|
|
html += `<div style="background:#0d1a0a;border:1px solid #1a3a0a;border-radius:3px;
|
|
|
|
|
padding:4px 8px;margin-bottom:5px;">
|
|
|
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px;">
|
|
|
|
|
<span style="font-size:10px;color:#8bc34a;font-weight:600;">⟳ ${a.profile}</span>
|
|
|
|
|
<span style="font-size:10px;color:#8bc34a;font-weight:600;">⟳ ${vvEscHtml(a.profile)}</span>
|
|
|
|
|
<span style="font-size:10px;color:#6a8a4a;">${_dur(sec)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="height:3px;background:#0a0a0a;border-radius:2px;overflow:hidden;">
|
|
|
|
@@ -1529,7 +1539,7 @@ function vvPollMonitor() {
|
|
|
|
|
|
|
|
|
|
html += `<div style="margin-bottom:4px;">
|
|
|
|
|
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:2px;">
|
|
|
|
|
<span style="font-size:10px;color:#555;width:46px;flex-shrink:0;">${meta.label}</span>
|
|
|
|
|
<span style="font-size:10px;color:#555;width:46px;flex-shrink:0;">${vvEscHtml(meta.label)}</span>
|
|
|
|
|
<span style="font-size:9px;color:#333;flex:1;text-align:right;margin-right:6px;">
|
|
|
|
|
${s?.duration ? _dur(s.duration) : ''}</span>
|
|
|
|
|
<span style="font-size:9px;color:#2a2a2a;width:28px;text-align:right;margin-right:5px;">
|
|
|
|
@@ -1607,7 +1617,7 @@ function vvPollMonitor() {
|
|
|
|
|
bodyEl.innerHTML =
|
|
|
|
|
// header row: name + process count pill
|
|
|
|
|
`<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
|
|
|
|
|
<div style="font-size:11px;color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">${gpu.name}</div>
|
|
|
|
|
<div style="font-size:11px;color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">${vvEscHtml(gpu.name)}</div>
|
|
|
|
|
<div style="margin-left:8px;background:${procColor}22;border:1px solid ${procColor};color:${procColor};
|
|
|
|
|
padding:1px 8px;border-radius:10px;font-size:11px;white-space:nowrap;">${procCount} proc${procCount !== 1 ? 's' : ''}</div>
|
|
|
|
|
</div>` +
|
|
|
|
@@ -1699,7 +1709,7 @@ function vvPollMonitor() {
|
|
|
|
|
const meth = s.method.replace('Transcode', 'TC').replace('Direct ', '');
|
|
|
|
|
return `<div style="display:flex;align-items:center;gap:5px;margin-bottom:4px;min-width:0;overflow:hidden;">
|
|
|
|
|
${vvSrvIcon(s.server_type, s.server)}
|
|
|
|
|
<span style="font-size:10px;color:#888;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${s.title}</span>
|
|
|
|
|
<span style="font-size:10px;color:#888;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${vvEscHtml(s.title)}</span>
|
|
|
|
|
<span style="font-size:9px;color:#555;flex-shrink:0;">${typeLabel(s.type)}</span>
|
|
|
|
|
<span style="font-size:9px;color:#ff9800;flex-shrink:0;">${meth}</span>
|
|
|
|
|
</div>`;
|
|
|
|
@@ -1799,15 +1809,15 @@ function vvPollFast() {
|
|
|
|
|
const peakTx = Math.max(...vvNetTxHistory, 0);
|
|
|
|
|
|
|
|
|
|
const ipRows = [
|
|
|
|
|
net.local_ip ? `<div><span style="color:#555;font-size:9px;">LAN </span>${net.local_ip}</div>` : '',
|
|
|
|
|
net.ext_ip ? `<div><span style="color:#555;font-size:9px;">EXT </span>${net.ext_ip}</div>` : '',
|
|
|
|
|
net.ts_ip ? `<div><span style="color:#555;font-size:9px;">TS </span>${net.ts_ip}</div>` : '',
|
|
|
|
|
net.local_ip ? `<div><span style="color:#555;font-size:9px;">LAN </span>${vvEscHtml(net.local_ip)}</div>` : '',
|
|
|
|
|
net.ext_ip ? `<div><span style="color:#555;font-size:9px;">EXT </span>${vvEscHtml(net.ext_ip)}</div>` : '',
|
|
|
|
|
net.ts_ip ? `<div><span style="color:#555;font-size:9px;">TS </span>${vvEscHtml(net.ts_ip)}</div>` : '',
|
|
|
|
|
].filter(Boolean).join('');
|
|
|
|
|
|
|
|
|
|
document.getElementById('vv-network-body').innerHTML =
|
|
|
|
|
`<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;">
|
|
|
|
|
<div>
|
|
|
|
|
<div style="font-size:12px;color:#888;margin-bottom:4px;">${net.iface} · ${linkLabel}</div>
|
|
|
|
|
<div style="font-size:12px;color:#888;margin-bottom:4px;">${vvEscHtml(net.iface)} · ${linkLabel}</div>
|
|
|
|
|
<div style="display:flex;gap:16px;font-size:13px;font-weight:600;">
|
|
|
|
|
<span><span style="color:#4caf50;font-size:9px;margin-right:4px;">━ IN (RX)</span><span style="color:#4caf50;">${vvFmtBps(rx)}</span><span style="color:#444;font-size:9px;font-weight:400;margin-left:4px;">peak ${vvFmtBps(peakRx)}</span></span>
|
|
|
|
|
<span><span style="color:#ff9800;font-size:9px;margin-right:4px;">━ OUT (TX)</span><span style="color:#ff9800;">${vvFmtBps(tx)}</span><span style="color:#444;font-size:9px;font-weight:400;margin-left:4px;">peak ${vvFmtBps(peakTx)}</span></span>
|
|
|
|
@@ -1886,8 +1896,8 @@ function vvRenderStreams() {
|
|
|
|
|
const badges = names.map(n => {
|
|
|
|
|
const cnt = serverCounts[n] ?? 0;
|
|
|
|
|
return cnt > 0
|
|
|
|
|
? `<span class="vv-server-badge">${n} <b style="color:#ccc;">${cnt}</b></span>`
|
|
|
|
|
: `<span class="vv-server-badge" style="color:#444;">${n}</span>`;
|
|
|
|
|
? `<span class="vv-server-badge">${vvEscHtml(n)} <b style="color:#ccc;">${cnt}</b></span>`
|
|
|
|
|
: `<span class="vv-server-badge" style="color:#444;">${vvEscHtml(n)}</span>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
// Per-chip shade: alternate bg brightness within a group to visually separate chips
|
|
|
|
@@ -1902,8 +1912,11 @@ function vvRenderStreams() {
|
|
|
|
|
const bg = (shades[cls] ?? ['',''])[i % 2];
|
|
|
|
|
return bg ? ` style="background:${bg};"` : '';
|
|
|
|
|
}
|
|
|
|
|
// Escapes here rather than at the four call sites: every one passes plain text, and the labels
|
|
|
|
|
// are not all ours — an unrecognised codec falls through vvCodecLabel() as the media server
|
|
|
|
|
// spelled it, and the device type is derived from the client string the player reports.
|
|
|
|
|
function vvChip(cls, label, i) {
|
|
|
|
|
return `<span class="${cls}"${vvChipShade(cls, i)}>${label}</span>`;
|
|
|
|
|
return `<span class="${cls}"${vvChipShade(cls, i)}>${vvEscHtml(label)}</span>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Device type summary — e.g. "3 Android 1 iOS 2 Roku"
|
|
|
|
@@ -2008,11 +2021,11 @@ function vvRenderStreams() {
|
|
|
|
|
return `<div>
|
|
|
|
|
<div style="display:flex;justify-content:space-between;align-items:center;font-size:11px;margin-bottom:2px;">
|
|
|
|
|
<span style="color:${s.paused ? '#fdd835' : '#aaa'};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">
|
|
|
|
|
<span style="color:${iconColor};">${icon}</span> ${s.title}</span>
|
|
|
|
|
<span style="color:#444;font-size:10px;margin-left:6px;flex-shrink:0;">${s.server}</span>
|
|
|
|
|
<span style="color:${iconColor};">${icon}</span> ${vvEscHtml(s.title)}</span>
|
|
|
|
|
<span style="color:#444;font-size:10px;margin-left:6px;flex-shrink:0;">${vvEscHtml(s.server)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div style="display:flex;justify-content:space-between;font-size:10px;color:#555;margin-bottom:3px;">
|
|
|
|
|
<span>${s.user}</span>
|
|
|
|
|
<span>${vvEscHtml(s.user)}</span>
|
|
|
|
|
${timeStr}
|
|
|
|
|
</div>
|
|
|
|
|
<div style="background:#1a1a1a;border-radius:3px;height:6px;overflow:hidden;">
|
|
|
|
@@ -2223,7 +2236,14 @@ function vvToggleContainer(name) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function vvDockerAction(action, name, webui) {
|
|
|
|
|
if (action === 'webui') { window.open(webui, '_blank'); return; }
|
|
|
|
|
// Filtered again at the point of use, not only where the button was built. This value originates
|
|
|
|
|
// in a container's template XML, and window.open() on a javascript: URL runs it with this page's
|
|
|
|
|
// origin — the one sink where an unchecked scheme is not merely a broken link.
|
|
|
|
|
if (action === 'webui') {
|
|
|
|
|
const u = vvSafeUrl(webui);
|
|
|
|
|
if (u) window.open(u, '_blank', 'noopener');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (action === 'edit') {
|
|
|
|
|
window.location.href = '/Docker?action=template&xmlTemplate=' +
|
|
|
|
|
encodeURIComponent('/boot/config/plugins/dockerMan/templates-user/my-' + name + '.xml') + '&update=true';
|
|
|
|
@@ -2270,7 +2290,7 @@ function vvRenderDockerFolders(data) {
|
|
|
|
|
html += `<div class="vv-df-vm-row">
|
|
|
|
|
<span class="vv-df-vm-icon">${osIcon(vm.os)}</span>
|
|
|
|
|
<span style="width:7px;height:7px;border-radius:50%;background:${sc};flex-shrink:0;${pulse}"></span>
|
|
|
|
|
<span class="vv-df-cname">${vm.name}</span>
|
|
|
|
|
<span class="vv-df-cname">${vvEscHtml(vm.name)}</span>
|
|
|
|
|
<span style="font-size:11px;font-weight:600;color:${sc};flex-shrink:0;">${stateLabel(vm.state)}</span>
|
|
|
|
|
${meta}
|
|
|
|
|
</div>`;
|
|
|
|
@@ -2288,8 +2308,14 @@ function vvRenderDockerFolders(data) {
|
|
|
|
|
const pulse = c.running ? 'animation:vv-pulse-dot 1s ease-in-out infinite;' : '';
|
|
|
|
|
const active = vvDfActive === c.name;
|
|
|
|
|
const sShort = c.status ? c.status.replace(/^Up\s+/, '').split(' ').slice(0,2).join(' ') : '—';
|
|
|
|
|
const sn = c.name.replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
|
|
|
|
const sw = (c.webui||'').replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
|
|
|
|
// Escaped for two nested contexts at once: a JS string literal, and the double-quoted onclick
|
|
|
|
|
// attribute holding it. The previous version did the first half only — \ and ' — which leaves
|
|
|
|
|
// a " free to close the attribute and destroy every handler after it. Docker's own charset
|
|
|
|
|
// makes that unreachable through a container name, but the WebUI value comes from template
|
|
|
|
|
// XML and is under no such constraint.
|
|
|
|
|
const jsq = v => vvEscAttr(String(v ?? '').replace(/\\/g,'\\\\').replace(/'/g,"\\'"));
|
|
|
|
|
const sn = jsq(c.name);
|
|
|
|
|
const sw = jsq(vvSafeUrl(c.webui));
|
|
|
|
|
|
|
|
|
|
let actionBar = '';
|
|
|
|
|
if (active) {
|
|
|
|
@@ -2309,8 +2335,8 @@ function vvRenderDockerFolders(data) {
|
|
|
|
|
return `<div class="vv-df-container${active ? ' vv-df-active' : ''}"
|
|
|
|
|
onclick="event.stopPropagation();vvToggleContainer('${sn}')">
|
|
|
|
|
<span class="vv-df-dot" style="background:${dot};${pulse}"></span>
|
|
|
|
|
<span class="vv-df-cname">${c.name}</span>
|
|
|
|
|
<span class="vv-df-status">${sShort}</span>
|
|
|
|
|
<span class="vv-df-cname">${vvEscHtml(c.name)}</span>
|
|
|
|
|
<span class="vv-df-status">${vvEscHtml(sShort)}</span>
|
|
|
|
|
</div>${actionBar}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -2320,20 +2346,20 @@ function vvRenderDockerFolders(data) {
|
|
|
|
|
const running = f.containers.filter(c => c.running).length;
|
|
|
|
|
const bColor = running === total ? '#4caf50' : running === 0 ? '#555' : '#ff9800';
|
|
|
|
|
const badge = `<span style="font-size:10px;color:${bColor};flex-shrink:0;margin-left:auto;">${running}/${total}</span>`;
|
|
|
|
|
const sid = f.id.replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
|
|
|
|
const sid = vvEscAttr(String(f.id ?? '').replace(/\\/g,'\\\\').replace(/'/g,"\\'"));
|
|
|
|
|
|
|
|
|
|
let iconHtml = '';
|
|
|
|
|
if (f.isEmoji) {
|
|
|
|
|
iconHtml = `<span style="font-size:11px;flex-shrink:0;">${f.icon}</span>`;
|
|
|
|
|
} else if (f.icon) {
|
|
|
|
|
iconHtml = `<img src="${f.icon}" style="width:13px;height:13px;object-fit:contain;border-radius:2px;flex-shrink:0;" onerror="this.style.display='none'">`;
|
|
|
|
|
iconHtml = `<span style="font-size:11px;flex-shrink:0;">${vvEscHtml(f.icon)}</span>`;
|
|
|
|
|
} else if (vvSafeUrl(f.icon)) {
|
|
|
|
|
iconHtml = `<img src="${vvEscAttr(vvSafeUrl(f.icon))}" style="width:13px;height:13px;object-fit:contain;border-radius:2px;flex-shrink:0;" onerror="this.style.display='none'">`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let out = `<div class="vv-df-folder">
|
|
|
|
|
<div class="vv-df-folder-hdr" onclick="vvToggleFolder('${sid}')">
|
|
|
|
|
<span class="vv-df-chevron">${open ? '▾' : '▸'}</span>
|
|
|
|
|
${iconHtml}
|
|
|
|
|
<span class="vv-df-fname">${f.name}</span>
|
|
|
|
|
<span class="vv-df-fname">${vvEscHtml(f.name)}</span>
|
|
|
|
|
${badge}
|
|
|
|
|
</div>`;
|
|
|
|
|
if (open) {
|
|
|
|
|