async function loadShares() {
const res = await apiFetch('/api/shares');
if (!res) return;
const shares = await res.json();
const sb = document.getElementById('smbTableBody');
if (!shares.smb?.length) {
sb.innerHTML = `
| No SMB shares. |
`;
} else {
sb.innerHTML = shares.smb.map(s => {
const enabled = s.enabled !== false;
const dot = ``;
// esc() HTML-encodes JSON so " inside onclick="..." doesn't break the attribute
const shareJson = esc(JSON.stringify(s));
const nameJson = esc(JSON.stringify(s.name));
return `
| ${esc(s.name)} |
${esc(s.path)} |
${esc(s.valid_users || '—')} |
${dot} |
|
`;
}).join('');
}
const nb = document.getElementById('nfsTableBody');
if (!shares.nfs?.length) {
nb.innerHTML = `| No NFS exports. |
`;
} else {
nb.innerHTML = shares.nfs.map(s => {
const enabled = s.enabled !== false;
const dot = ``;
const shareJson = esc(JSON.stringify(s));
const pathJson = esc(JSON.stringify(s.path));
return `
${esc(s.path)} |
${esc(s.clients)} |
${dot} |
|
`;
}).join('');
}
}
async function addSMBShare() {
const name = document.getElementById('smbName').value.trim();
const path = document.getElementById('smbPath').value.trim();
const validUsers = document.getElementById('smbUsers').value.trim();
if (!name || !path) return showToast('Name and path are required.', 'danger');
const res = await apiFetch('/api/share/smb', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name, path, validUsers}),
});
if (!res) return;
const data = await res.json();
if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger');
showToast('SMB share created.');
document.getElementById('smbName').value = '';
document.getElementById('smbPath').value = '';
document.getElementById('smbUsers').value = '';
loadShares(); loadDashboard();
}
async function addNFSShare() {
const path = document.getElementById('nfsPath').value.trim();
const clients = document.getElementById('nfsClients').value.trim() || '*';
const readOnly = document.getElementById('nfsReadOnly').checked;
if (!path) return showToast('Path is required.', 'danger');
const res = await apiFetch('/api/share/nfs', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({path, clients, readOnly}),
});
if (!res) return;
const data = await res.json();
if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger');
showToast('NFS export created.');
document.getElementById('nfsPath').value = '';
document.getElementById('nfsClients').value = '';
document.getElementById('nfsReadOnly').checked = false;
loadShares(); loadDashboard();
}
// ===== EDIT =====
let _editCtx = null;
async function openEditModal(type, share) {
_editCtx = { type, original: share };
document.getElementById('editModalTitle').textContent =
type === 'smb' ? `Edit SMB Share — ${esc(share.name)}` : `Edit NFS Export — ${esc(share.path)}`;
document.getElementById('editModalBody').innerHTML =
`Loading…
`;
new bootstrap.Modal(document.getElementById('editModal')).show();
const [ur, gr] = await Promise.all([apiFetch('/api/users'), apiFetch('/api/groups')]);
const users = ur ? await ur.json() : [];
const groups = gr ? await gr.json() : [];
const ownerOptions = ['root', ...users.map(u => u.name)]
.map(n => `