Auth page: pure-PHP Authelia ACL parser, NPM-sourced certs tab, watchdog JS fixes, rsync settings layout

- Replace python3/PyYAML Authelia ACL parser with pure PHP (no deps available on Unraid)
- Cert tab now pulls live from NPM API instead of cert_monitor.sh — auto-discovers all managed certs sorted by urgency
- Watchdog page: add missing GB constant and _fmtBytes/_relTime functions that were causing silent render failure
- Rsync settings card: pin to far-right 3 columns (grid-column:6/-1), toggle grid narrowed to 2 columns
- Add CLAUDE.md project context file on /boot for session persistence across reboots
- claude_startup.sh: symlink CLAUDE.md into /root on array start
This commit is contained in:
Gmer4Lfe
2026-06-06 15:39:34 -04:00
parent 9c3ace95a7
commit bac0f7c535
7 changed files with 425 additions and 113 deletions
+27 -34
View File
@@ -214,14 +214,11 @@ $isOwner = vv_is_owner();
<div class="vv-au-panel" id="vv-au-panel-certs">
<div class="vv-au-sec-bar">
<span class="vv-au-sec-title" id="vv-au-cert-ts"></span>
<button class="vv-au-btn prim" id="vv-au-cert-run">Run now</button>
<button class="vv-au-btn prim" id="vv-au-cert-run">Refresh</button>
</div>
<div class="vv-au-cert-grid" id="vv-au-cert-grid">
<div class="vv-au-loading">Loading…</div>
</div>
<div id="vv-au-cert-log" style="display:none;margin-top:12px;background:#0d0d0d;border:1px solid #1e1e1e;
border-radius:4px;padding:10px 12px;font-size:10px;color:#555;font-family:monospace;
max-height:140px;overflow-y:auto;white-space:pre-wrap;"></div>
<div id="vv-au-cert-cfg" style="margin-top:10px;font-size:10px;color:#3a3a3a;"></div>
</div>
@@ -954,29 +951,31 @@ function _certRel(ts) {
return Math.floor(d/86400) + 'd ago';
}
function _renderCerts(data) {
function _renderNpmCerts(data) {
const grid = document.getElementById('vv-au-cert-grid');
const ts = document.getElementById('vv-au-cert-ts');
const cfg = document.getElementById('vv-au-cert-cfg');
const warn = data.warn_days || 30, crit = data.crit_days || 7;
const certs = data.certs || [];
ts.textContent = data.checked_at ? 'Last checked: ' + _certRel(data.checked_at) : 'Not yet checked';
ts.textContent = certs.length + ' certificate' + (certs.length !== 1 ? 's' : '') + ' · live from NPM';
cfg.textContent = `Warn: ${warn}d · Crit: ${crit}d`;
const domains = data.domains || [];
if (!domains.length) {
grid.innerHTML = '<div class="vv-au-empty">No domains configured — add HOST*_CERT_MONITOR_DOMAINS to host.conf</div>';
if (!certs.length) {
grid.innerHTML = '<div class="vv-au-empty">No certificates found in NPM</div>';
return;
}
grid.innerHTML = domains.map(d => {
const s = d.status || 'UNKN';
const days = d.days;
grid.innerHTML = certs.map(c => {
const s = c.status || 'UNKN';
const days = c.days;
const col = _certDayColor(days, warn, crit);
const barPct = days != null ? Math.min(Math.round(days/90*100), 100) : 0;
const expStr = d.expires ? 'Expires ' + d.expires : (s === 'UNKN' ? 'Not yet checked' : '');
const barPct = days != null ? Math.min(Math.round(days / 90 * 100), 100) : 0;
const expStr = c.expires ? 'Expires ' + c.expires : '';
const extra = (c.domain_names || []).filter(d => d !== c.nice_name).join(', ');
return `<div class="vv-au-card" style="padding:12px 14px;">
<div class="vv-au-domain">${_esc(d.domain)}</div>
<div class="vv-au-domain">${_esc(c.nice_name)}</div>
${extra ? `<div style="font-size:9px;color:#444;margin-bottom:2px;word-break:break-all;">${_esc(extra)}</div>` : ''}
<div class="vv-au-cert-days" style="color:${col}">${days != null ? days : '—'}</div>
<div style="font-size:9px;color:#444;margin-bottom:6px;">${days != null ? 'days remaining' : ''}</div>
<span class="vv-au-badge ${_certBadgeCls(s)}">${_certBadgeTxt(s)}</span>
@@ -986,32 +985,26 @@ function _renderCerts(data) {
}).join('');
}
function _loadCerts() {
function _loadCerts(onDone) {
const grid = document.getElementById('vv-au-cert-grid');
grid.innerHTML = '<div class="vv-au-loading">Loading…</div>';
fetch(CERT_API)
fetch(CERT_API + '?action=npm')
.then(r => r.json())
.then(d => { if (d.ok) _renderCerts(d); })
.catch(() => { grid.innerHTML = '<div class="vv-au-empty" style="color:#ef5350">Failed to load cert data</div>'; });
.then(d => {
if (d.ok) _renderNpmCerts(d);
else throw new Error(d.error || 'NPM error');
if (onDone) onDone();
})
.catch(e => {
grid.innerHTML = `<div class="vv-au-empty" style="color:#ef5350">${_esc(e.message || 'Failed to load')}</div>`;
if (onDone) onDone();
});
}
document.getElementById('vv-au-cert-run').addEventListener('click', function() {
const btn = this;
const log = document.getElementById('vv-au-cert-log');
btn.disabled = true; btn.textContent = 'Checking…';
log.style.display = 'none'; log.textContent = '';
const fd = new FormData(); fd.append('action', 'run');
fetch(CERT_API, { method: 'POST', body: fd })
.then(r => r.json())
.then(d => {
btn.disabled = false; btn.textContent = 'Run now';
if (d.data) _renderCerts(d.data);
if (d.output && d.output.length) {
log.style.display = 'block';
log.textContent = d.output.join('\n').replace(/\x1b\[[0-9;]*m/g, '');
}
})
.catch(() => { btn.disabled = false; btn.textContent = 'Run now'; });
btn.disabled = true; btn.textContent = 'Loading…';
_loadCerts(() => { btn.disabled = false; btn.textContent = 'Refresh'; });
});
// ── Boot ──────────────────────────────────────────────────────────────────────
+4 -22
View File
@@ -44,8 +44,7 @@
.vv-ry-run-st { text-align:right; }
/* Settings */
.vv-ry-set-grid { display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin-top:6px; }
@media (max-width:900px) { .vv-ry-set-grid { grid-template-columns:repeat(2,1fr); } }
.vv-ry-set-grid { display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-top:6px; }
.vv-ry-toggle { display:inline-flex;align-items:center;gap:8px;cursor:pointer;user-select:none; }
.vv-ry-toggle-track { width:32px;height:18px;border-radius:9px;background:#222;border:1px solid #333;position:relative;transition:background .15s,border-color .15s;flex-shrink:0; }
.vv-ry-toggle-track.on { background:#1a3a1a;border-color:#2d5a2d; }
@@ -534,15 +533,6 @@ const PROF_COLORS = [
'#4dd0e1','#a5d6a7','#ff8a65','#90caf9','#f48fb1',
];
function _rel(ts) {
if (!ts) return '—';
const d = Math.floor(Date.now() / 1000) - ts;
if (d < 60) return 'just now';
if (d < 3600) return Math.floor(d / 60) + 'm ago';
if (d < 86400) return Math.floor(d / 3600) + 'h ' + Math.floor((d % 3600) / 60) + 'm ago';
return Math.floor(d / 86400) + 'd ago';
}
function _dur(s) {
if (!s) return '—';
const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
@@ -551,14 +541,6 @@ function _dur(s) {
return sec + 's';
}
function _fmtBytes(b) {
if (!b) return '';
const G = 1073741824, M = 1048576;
if (b >= G) return (b / G).toFixed(1) + ' GB';
if (b >= M) return (b / M).toFixed(0) + ' MB';
return (b / 1024).toFixed(0) + ' KB';
}
// ── Status + active card ──────────────────────────────────────────────────────
function _statusCard(data) {
const en = data.enabled;
@@ -609,7 +591,7 @@ function _lastSyncCard(data) {
<span class="vv-ry-lbl">${WIN_META[k]?.label || k}</span>
<span class="vv-ry-val">
<span style="${cls}">${e.status}</span>
<span style="color:#444;margin-left:6px;">${_rel(e.ts)}</span>
<span style="color:#444;margin-left:6px;">${_relTime(e.ts)}</span>
${e.duration ? `<span style="color:#3a3a3a;margin-left:4px;">${_dur(e.duration)}</span>` : ''}
</span>
</div>`;
@@ -639,7 +621,7 @@ function _windowsRow(data) {
if (ls && ls.ts) {
const okCol = ls.status === 'success' ? '#4caf50' : '#ef5350';
statusHtml = `<div class="vv-ry-win-stat" style="color:${okCol};">${ls.status}</div>
<div class="vv-ry-win-dur">${_rel(ls.ts)}${ls.duration ? ' · ' + _dur(ls.duration) : ''}</div>`;
<div class="vv-ry-win-dur">${_relTime(ls.ts)}${ls.duration ? ' · ' + _dur(ls.duration) : ''}</div>`;
}
const expandable = !!(scripts.length || shares.length);
@@ -755,7 +737,7 @@ function _settingsSection(data) {
const mbit = s.bw_limit ? ' (~' + (s.bw_limit / 125).toFixed(0) + ' Mbit/s)' : '';
return `<div class="vv-ry-card" style="grid-column:1/-1;">
return `<div class="vv-ry-card" style="grid-column:6/-1;">
<div class="vv-ry-sec" style="margin-bottom:10px;">Settings</div>
<div style="margin-bottom:12px;">
+16 -12
View File
@@ -55,19 +55,20 @@
const GB = 1073741824;
function _relTime(ts) {
if (!ts) return '';
const d = Math.floor(Date.now() / 1000) - ts;
if (d < 60) return 'just now';
if (d < 3600) return Math.floor(d / 60) + 'm ago';
if (d < 86400)return Math.floor(d / 3600) + 'h ' + Math.floor((d % 3600) / 60) + 'm ago';
return Math.floor(d / 86400) + 'd ago';
function _fmtBytes(b) {
if (!b) return '0';
if (b >= GB) return (b / GB).toFixed(1) + 'G';
if (b >= 1048576) return (b / 1048576).toFixed(0) + 'M';
return (b / 1024).toFixed(0) + 'K';
}
function _fmtBytes(b) {
if (b >= GB) return (b / GB).toFixed(1) + ' GB';
if (b >= 1048576) return (b / 1048576).toFixed(0) + ' MB';
return (b / 1024).toFixed(0) + ' KB';
function _relTime(ts) {
if (!ts) return '—';
const s = Math.floor(Date.now() / 1000) - ts;
if (s < 60) return 'just now';
if (s < 3600) return Math.floor(s / 60) + 'm ago';
if (s < 86400) return Math.floor(s / 3600) + 'h ago';
return Math.floor(s / 86400) + 'd ago';
}
function _dur(s) {
@@ -493,7 +494,10 @@ function vvWdLoad() {
fetch('/plugins/varaverk/api/watchdog.php')
.then(r => r.json())
.then(_render)
.catch(() => {});
.catch(() => {
document.getElementById('vv-wd-grid').innerHTML =
'<div style="grid-column:1/-1;color:#555;font-size:12px;padding:24px 0;text-align:center;">Error loading watchdog data — check API</div>';
});
}
vvWdLoad();