Add dashboard IPs/restart/connections, SMB read-only and recycle bin

Dashboard:
- Server IPs card showing all non-loopback IPv4 interfaces
- Restart buttons next to smbd/nfsd status dots (POST /api/service/<name>/restart)
- Active SMB connections table (smbstatus -b parsed in backend)

SMB edit modal:
- Read only toggle (writable vs read only in smb.conf)
- Recycle Bin toggle (vfs objects = recycle, files go to .recycle folder)

Docs: PLANNING.md phase 3.5, README recycle bin + read-only, CLAUDE.md JS overview

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Sonnet 4.6 committed 2026-06-20 01:13:32 +02:00
1 parent 1810d60d21
commit 1be6a05cef
7 files changed
+171 -21

No files matched your search

+40 -4
View File
@@ -3,14 +3,20 @@ async function loadDashboard() {
if (!res) return;
const d = await res.json();
function setDaemon(id, running) {
// Service status cards with restart buttons
function setDaemon(id, service, running) {
document.getElementById(id).innerHTML =
`<span class="dot ${running ? 'dot-green' : 'dot-red'}"></span>
<span class="small">${running ? 'Running' : 'Stopped'}</span>`;
<span class="small me-2">${running ? 'Running' : 'Stopped'}</span>
<button class="btn btn-sm ms-auto" style="border:1px solid var(--border); color:var(--muted); padding:1px 8px; font-size:.75rem"
onclick="restartService('${service}')">Restart</button>`;
document.getElementById(id).style.display = 'flex';
document.getElementById(id).style.alignItems = 'center';
}
setDaemon('statusSmbd', d.status.smbd);
setDaemon('statusNfsd', d.status.nfsd);
setDaemon('statusSmbd', 'smbd', d.status.smbd);
setDaemon('statusNfsd', 'nfsd', d.status.nfsd);
// Disk bar
if (d.disk && !d.disk.error) {
const p = d.disk.percent;
const bar = document.getElementById('diskBar');
@@ -22,6 +28,15 @@ async function loadDashboard() {
document.getElementById('diskLabel').textContent = 'Error: ' + d.disk.error;
}
// Server IPs
const ipEl = document.getElementById('serverIps');
if (ipEl) {
ipEl.innerHTML = d.ips?.length
? d.ips.map(i => `<code class="me-3">${esc(i.iface)}: ${esc(i.ip)}</code>`).join('')
: '<span style="color:var(--muted)">—</span>';
}
// Shares table
const sb = document.getElementById('dashSharesBody');
if (!d.shares?.length) {
sb.innerHTML = `<tr><td colspan="5" style="color:var(--muted)">No shares configured.</td></tr>`;
@@ -36,5 +51,26 @@ async function loadDashboard() {
</tr>`).join('');
}
// Active connections
const connEl = document.getElementById('activeConnectionsBody');
if (connEl) {
connEl.innerHTML = d.connections?.length
? d.connections.map(c => `
<tr>
<td>${esc(c.user)}</td>
<td style="color:var(--muted)">${esc(c.machine)}</td>
</tr>`).join('')
: `<tr><td colspan="2" style="color:var(--muted)">No active connections.</td></tr>`;
}
_startRefreshCountdown();
}
async function restartService(name) {
const res = await apiFetch(`/api/service/${name}/restart`, { method: 'POST' });
if (!res) return;
const data = await res.json();
if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger');
showToast(`${name} restarted.`);
loadDashboard();
}
+15 -5
View File
@@ -187,10 +187,18 @@ async function openEditModal(type, share) {
</select>
</div>
</div>
<div class="form-check ms-1 mb-3">
<div class="form-check ms-1 mb-1">
<input type="checkbox" id="editReadOnly" class="form-check-input" ${share.read_only ? 'checked' : ''}>
<label class="form-check-label" for="editReadOnly" style="color:var(--muted)">Read only — clients cannot write</label>
</div>
<div class="form-check ms-1 mb-1">
<input type="checkbox" id="editGuestOk" class="form-check-input" ${share.guest_ok ? 'checked' : ''}>
<label class="form-check-label" for="editGuestOk" style="color:var(--muted)">Guest OK — allow access without a password</label>
</div>
<div class="form-check ms-1 mb-3">
<input type="checkbox" id="editRecycleBin" class="form-check-input" ${share.recycle_bin ? 'checked' : ''}>
<label class="form-check-label" for="editRecycleBin" style="color:var(--muted)">Recycle Bin — deleted files go to <code>.recycle</code> instead of being gone</label>
</div>
${permSection}`;
} else {
const isRo = /\(ro[,)]/.test(share.clients);
@@ -231,12 +239,14 @@ async function submitEdit() {
const name = document.getElementById('editName').value.trim();
const path = document.getElementById('editPath').value.trim();
const validUsers = document.getElementById('editUsers').value.trim();
const forceUser = document.getElementById('editForceUser').value;
const forceGroup = document.getElementById('editForceGroup').value;
const guestOk = document.getElementById('editGuestOk').checked;
const forceUser = document.getElementById('editForceUser').value;
const forceGroup = document.getElementById('editForceGroup').value;
const guestOk = document.getElementById('editGuestOk').checked;
const readOnly = document.getElementById('editReadOnly').checked;
const recycleBin = document.getElementById('editRecycleBin').checked;
res = await apiFetch(`/api/share/smb/${encodeURIComponent(ctx.original.name)}`, {
method: 'PATCH', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name, path, validUsers, forceUser, forceGroup, guestOk}),
body: JSON.stringify({name, path, validUsers, forceUser, forceGroup, guestOk, readOnly, recycleBin}),
});
} else {
const clients = document.getElementById('editClients').value.trim();