Add docker actions, arr profile enforcer, monitor caching, and web file symlink
Web files now served via symlink to the git repo so git pull changes survive reboots without rebuilding the txz. Also includes: docker pull/rebuild/restart with live log streaming, arr_profile_enforcer for Sonarr/Radarr quality profiles, monitor page cache fix (background writer now in cron), and ARR_KIDS/SONARR/RADARR profile name vars in master.conf.
This commit is contained in:
@@ -71,6 +71,21 @@
|
||||
.vv-dk-pop-item.current { color:#4caf50; }
|
||||
.vv-dk-pop-item.sep { border-top:1px solid #222;margin-top:4px;padding-top:8px; }
|
||||
.vv-dk-pop-item.blue { color:#5c9fd4; }
|
||||
.vv-dk-pop-item.red { color:#ef5350; }
|
||||
|
||||
/* Action button + job badge */
|
||||
.vv-dk-ctr-act-btn { font-size:11px;padding:0 5px;border-radius:3px;border:1px solid #222;background:transparent;color:#333;cursor:pointer;line-height:1.7;margin-left:auto;flex-shrink:0; }
|
||||
.vv-dk-ctr-act-btn:hover { background:#1e1e1e;color:#777; }
|
||||
.vv-dk-job-badge { font-size:10px;padding:1px 6px;border-radius:2px;white-space:nowrap;flex-shrink:0; }
|
||||
.vv-dk-job-badge.pulling { background:#0d1a2a;color:#5c7cfa; }
|
||||
.vv-dk-job-badge.rebuilding { background:#1a1a0a;color:#cddc39; }
|
||||
.vv-dk-job-badge.restarting { background:#1a1a2a;color:#9c89d4; }
|
||||
.vv-dk-job-badge.done-ok { background:#0d1a0d;color:#4caf50; }
|
||||
.vv-dk-job-badge.done-err { background:#1a0d0d;color:#ef5350; }
|
||||
|
||||
/* Log panel */
|
||||
.vv-dk-log-panel { padding:8px 12px;background:#080808;border-top:1px solid #161616; }
|
||||
.vv-dk-log-pre { margin:0;font-size:10px;color:#4a4a4a;font-family:monospace;white-space:pre-wrap;word-break:break-all;max-height:260px;overflow-y:auto; }
|
||||
</style>
|
||||
|
||||
<div class="vv-dk-toolbar">
|
||||
@@ -125,6 +140,9 @@ function _ctrRow(c, folderId) {
|
||||
const statusLbl = c.running ? 'RUNNING' : (c.status || 'STOPPED').toUpperCase();
|
||||
const rowCls = 'vv-dk-ctr' + (c.running ? '' : ' stopped') + (_editMode ? ' edit-mode' : '');
|
||||
const editAttr = _editMode ? `data-ctr="${_esc(c.name)}" data-folder="${folderId||''}" title="Move ${c.name}"` : '';
|
||||
const ctrAttr = `data-ctr-name="${_esc(c.name)}"`;
|
||||
const actBtn = !_editMode ? `<button class="vv-dk-ctr-act-btn" data-act-ctr="${_esc(c.name)}" title="Actions">···</button>` : '';
|
||||
const jobBadge = !_editMode ? `<span class="vv-dk-job-badge" id="vv-dk-job-${_esc(c.name)}" style="display:none"></span>` : '';
|
||||
|
||||
// Icon or placeholder
|
||||
const iconHtml = c.icon
|
||||
@@ -161,17 +179,21 @@ function _ctrRow(c, folderId) {
|
||||
`</div>`;
|
||||
}
|
||||
|
||||
return `<div class="${rowCls}" ${editAttr}>
|
||||
return `<div class="${rowCls}" ${editAttr} ${ctrAttr}>
|
||||
${iconHtml}
|
||||
<div>
|
||||
<div class="vv-dk-ctr-name-row">
|
||||
<span class="vv-dk-ctr-name">${nameHtml}</span>
|
||||
<span class="vv-dk-ctr-status ${statusCls}">${statusLbl}</span>
|
||||
${jobBadge}${actBtn}
|
||||
</div>
|
||||
<div class="vv-dk-ctr-image">${_esc(_shortImage(c.image||''))}</div>
|
||||
${(netBadges || portBadges) ? `<div class="vv-dk-meta-row">${netBadges}${portBadges}${morePorts}</div>` : ''}
|
||||
${pathsHtml}
|
||||
</div>
|
||||
</div>
|
||||
<div class="vv-dk-log-panel" id="vv-dk-log-${_esc(c.name)}" style="display:none">
|
||||
<pre class="vv-dk-log-pre"></pre>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -255,9 +277,143 @@ function _render(data) {
|
||||
_bindEvents();
|
||||
}
|
||||
|
||||
// ── Container actions ─────────────────────────────────────────────────────────
|
||||
|
||||
let _activeJobs = {}; // { ctrName: intervalId }
|
||||
|
||||
function _actApi(params, cb) {
|
||||
const fd = new FormData();
|
||||
for (const [k, v] of Object.entries(params)) fd.append(k, v);
|
||||
fetch('/plugins/varaverk/api/docker_action.php', {method: 'POST', body: fd})
|
||||
.then(r => r.json()).then(cb)
|
||||
.catch(() => cb({ok: false, error: 'Request failed'}));
|
||||
}
|
||||
|
||||
function _jobBadgeEl(name) {
|
||||
return document.getElementById('vv-dk-job-' + name);
|
||||
}
|
||||
|
||||
function _setBadge(name, cls, text, autohide) {
|
||||
const el = _jobBadgeEl(name);
|
||||
if (!el) return;
|
||||
el.className = 'vv-dk-job-badge ' + cls;
|
||||
el.textContent = text;
|
||||
el.style.display = '';
|
||||
if (autohide) setTimeout(() => { if (el.parentNode) el.style.display = 'none'; }, 4000);
|
||||
}
|
||||
|
||||
function _pollJob(name, jobId) {
|
||||
_actApi({action: 'job_status', job_id: jobId}, data => {
|
||||
const st = data.status;
|
||||
if (st === 'pulling') _setBadge(name, 'pulling', 'Pulling…', false);
|
||||
if (st === 'rebuilding') _setBadge(name, 'rebuilding', 'Rebuilding…', false);
|
||||
if (st === 'done') {
|
||||
clearInterval(_activeJobs[name]);
|
||||
delete _activeJobs[name];
|
||||
if (data.ok) {
|
||||
_setBadge(name, 'done-ok', data.message || 'Done', true);
|
||||
} else {
|
||||
_setBadge(name, 'done-err', data.error || 'Failed', true);
|
||||
}
|
||||
setTimeout(_reload, 1500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _startJob(name, jobId) {
|
||||
if (_activeJobs[name]) clearInterval(_activeJobs[name]);
|
||||
_setBadge(name, 'pulling', 'Pulling…', false);
|
||||
_activeJobs[name] = setInterval(() => _pollJob(name, jobId), 2000);
|
||||
}
|
||||
|
||||
function _doRestart(name) {
|
||||
_setBadge(name, 'restarting', 'Restarting…', false);
|
||||
_actApi({action: 'restart', name}, data => {
|
||||
if (data.ok) {
|
||||
_setBadge(name, 'done-ok', 'Restarted', true);
|
||||
setTimeout(_reload, 1000);
|
||||
} else {
|
||||
_setBadge(name, 'done-err', 'Failed', true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _doStartStop(name, start) {
|
||||
_actApi({action: start ? 'start' : 'stop', name}, data => {
|
||||
if (data.ok) setTimeout(_reload, 800);
|
||||
else alert((start ? 'Start' : 'Stop') + ' failed: ' + (data.output || data.error || '?'));
|
||||
});
|
||||
}
|
||||
|
||||
function _doPullRebuild(name) {
|
||||
_actApi({action: 'pull_rebuild', name}, data => {
|
||||
if (data.ok && data.job_id) {
|
||||
_startJob(name, data.job_id);
|
||||
} else {
|
||||
alert('Update failed: ' + (data.error || '?'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _toggleLog(name) {
|
||||
const panel = document.getElementById('vv-dk-log-' + name);
|
||||
if (!panel) return;
|
||||
if (panel.style.display !== 'none') {
|
||||
panel.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
const pre = panel.querySelector('.vv-dk-log-pre');
|
||||
pre.textContent = 'Loading…';
|
||||
panel.style.display = '';
|
||||
_actApi({action: 'logs', name}, data => {
|
||||
pre.textContent = data.ok ? (data.logs || '(no output)') : ('Error: ' + (data.error || '?'));
|
||||
pre.scrollTop = pre.scrollHeight;
|
||||
});
|
||||
}
|
||||
|
||||
function _showActionsMenu(name, running, x, y) {
|
||||
const pop = document.getElementById('vv-dk-popover');
|
||||
let html = '';
|
||||
if (running) {
|
||||
html += `<div class="vv-dk-pop-item red" data-act="stop" data-act-n="${_esc(name)}">Stop</div>`;
|
||||
html += `<div class="vv-dk-pop-item" data-act="restart" data-act-n="${_esc(name)}">Restart</div>`;
|
||||
} else {
|
||||
html += `<div class="vv-dk-pop-item current" data-act="start" data-act-n="${_esc(name)}">Start</div>`;
|
||||
}
|
||||
html += `<div class="vv-dk-pop-item sep blue" data-act="pull_rebuild" data-act-n="${_esc(name)}">Update (Pull & Rebuild)</div>`;
|
||||
html += `<div class="vv-dk-pop-item sep" data-act="logs" data-act-n="${_esc(name)}">View Logs</div>`;
|
||||
pop.innerHTML = html;
|
||||
pop.style.display = 'block';
|
||||
const vw = window.innerWidth, vh = window.innerHeight;
|
||||
pop.style.left = Math.min(x, vw - 200) + 'px';
|
||||
pop.style.top = Math.min(y + 8, vh - 160) + 'px';
|
||||
|
||||
pop.querySelectorAll('[data-act]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const act = el.dataset.act, n = el.dataset.actN;
|
||||
_hidePopover();
|
||||
if (act === 'start' || act === 'stop') _doStartStop(n, act === 'start');
|
||||
else if (act === 'restart') _doRestart(n);
|
||||
else if (act === 'pull_rebuild') _doPullRebuild(n);
|
||||
else if (act === 'logs') _toggleLog(n);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Events ────────────────────────────────────────────────────────────────────
|
||||
|
||||
function _bindEvents() {
|
||||
// Action menu button (non-edit mode)
|
||||
document.querySelectorAll('[data-act-ctr]').forEach(btn => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const name = btn.dataset.actCtr;
|
||||
const allCtrs = [...(_data.folders||[]).flatMap(f=>f.containers), ...(_data.ungrouped||[])];
|
||||
const c = allCtrs.find(x => x.name === name);
|
||||
_showActionsMenu(name, c?.running ?? false, e.clientX, e.clientY);
|
||||
});
|
||||
});
|
||||
|
||||
// Container click in edit mode → folder picker
|
||||
document.querySelectorAll('.vv-dk-ctr.edit-mode').forEach(el => {
|
||||
el.addEventListener('click', e => {
|
||||
|
||||
Reference in New Issue
Block a user