- main.js: show live countdown "Updated HH:MM:SS — next in Xs" so the 30s auto-refresh is visually obvious instead of just a static timestamp - app.py: rename _find_mountpoint → _find_quota_fs and fix the stale call site that caused a NameError; function now returns mountpoint from df, falling back to the path itself if df returns / - README: add full quota setup section (fstab usrquota, quotacheck, quotaon, verify with repquota); fix stale "127.0.0.1" bind note (service already uses 0.0.0.0); update Auth section (sessions, not Basic Auth) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
62 lines
2.6 KiB
JavaScript
62 lines
2.6 KiB
JavaScript
async function loadDashboard() {
|
|
const res = await apiFetch('/api/dashboard');
|
|
if (!res) return;
|
|
const d = await res.json();
|
|
|
|
function setDaemon(id, running) {
|
|
document.getElementById(id).innerHTML =
|
|
`<span class="dot ${running ? 'dot-green' : 'dot-red'}"></span>
|
|
<span class="small">${running ? 'Running' : 'Stopped'}</span>`;
|
|
}
|
|
setDaemon('statusSmbd', d.status.smbd);
|
|
setDaemon('statusNfsd', d.status.nfsd);
|
|
|
|
if (d.disk && !d.disk.error) {
|
|
const p = d.disk.percent;
|
|
const bar = document.getElementById('diskBar');
|
|
bar.style.width = p + '%';
|
|
bar.style.background = p > 90 ? 'var(--red)' : p > 75 ? 'var(--yellow)' : 'var(--accent)';
|
|
document.getElementById('diskLabel').textContent =
|
|
`${d.disk.used_gb} GB / ${d.disk.total_gb} GB — ${d.disk.free_gb} GB free`;
|
|
} else if (d.disk?.error) {
|
|
document.getElementById('diskLabel').textContent = 'Error: ' + d.disk.error;
|
|
}
|
|
|
|
const sb = document.getElementById('dashSharesBody');
|
|
if (!d.shares?.length) {
|
|
sb.innerHTML = `<tr><td colspan="5" style="color:var(--muted)">No shares configured.</td></tr>`;
|
|
} else {
|
|
sb.innerHTML = d.shares.map(s => `
|
|
<tr>
|
|
<td><span class="badge rounded-pill ${s.type === 'smb' ? 'badge-smb' : 'badge-nfs'}">${s.type.toUpperCase()}</span></td>
|
|
<td>${esc(s.name ?? '—')}</td>
|
|
<td><code>${esc(s.path)}</code></td>
|
|
<td style="color:var(--muted); font-size:.87em">${esc(s.access || '—')}</td>
|
|
<td class="text-end" style="color:var(--muted)">${s.size_gb} GB</td>
|
|
</tr>`).join('');
|
|
}
|
|
|
|
const qb = document.getElementById('dashQuotasBody');
|
|
if (!d.quotas?.length) {
|
|
qb.innerHTML = `<tr><td colspan="4" style="color:var(--muted)">No quotas set.</td></tr>`;
|
|
} else {
|
|
qb.innerHTML = d.quotas.map(q => {
|
|
const p = Math.min(q.percent, 100);
|
|
const col = p > 90 ? 'var(--red)' : p > 75 ? 'var(--yellow)' : 'var(--green)';
|
|
return `<tr>
|
|
<td>${esc(q.user)}</td>
|
|
<td>${q.used_gb} GB</td>
|
|
<td>${q.soft_gb} GB</td>
|
|
<td>
|
|
<div class="progress" style="height:8px">
|
|
<div class="progress-bar" style="width:${p}%; background:${col}; transition:width .6s ease"></div>
|
|
</div>
|
|
<div class="mt-1" style="font-size:.75rem; color:var(--muted)">${p.toFixed(1)}%</div>
|
|
</td>
|
|
</tr>`;
|
|
}).join('');
|
|
}
|
|
|
|
_startRefreshCountdown();
|
|
}
|