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; 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)}`; let body = ''; if (type === 'smb') { body = `
`; } else { const isRo = /\(ro[,)]/.test(share.clients); const cleanClients = share.clients.replace(/\s*\(.*?\)/g, '').trim(); body = `
`; } document.getElementById('editModalBody').innerHTML = body; new bootstrap.Modal(document.getElementById('editModal')).show(); } async function submitEdit() { const ctx = _editCtx; if (!ctx) return; let res; if (ctx.type === 'smb') { const name = document.getElementById('editName').value.trim(); const path = document.getElementById('editPath').value.trim(); const validUsers = document.getElementById('editUsers').value.trim(); res = await apiFetch(`/api/share/smb/${encodeURIComponent(ctx.original.name)}`, { method: 'PATCH', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({name, path, validUsers}), }); } else { const clients = document.getElementById('editClients').value.trim(); const readOnly = document.getElementById('editReadOnly').checked; res = await apiFetch('/api/share/nfs', { method: 'PATCH', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({path: ctx.original.path, clients, readOnly}), }); } if (!res) return; const data = await res.json(); bootstrap.Modal.getInstance(document.getElementById('editModal')).hide(); if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger'); showToast('Share updated.'); loadShares(); loadDashboard(); } // ===== DELETE ===== function deleteShare(type, identifier) { const label = type === 'smb' ? `SMB share "${identifier}"` : `NFS export "${identifier}"`; document.getElementById('deleteModalMsg').textContent = `Are you sure you want to delete ${label}? This cannot be undone.`; const modal = new bootstrap.Modal(document.getElementById('deleteModal')); const btn = document.getElementById('deleteModalConfirm'); const handler = async () => { btn.removeEventListener('click', handler); modal.hide(); let res; if (type === 'smb') { res = await apiFetch(`/api/share/smb/${encodeURIComponent(identifier)}`, { method: 'DELETE' }); } else { res = await apiFetch('/api/share/nfs', { method: 'DELETE', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({path: identifier}), }); } if (!res) return; const data = await res.json(); if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger'); showToast(`${label} deleted.`); loadShares(); loadDashboard(); }; btn.addEventListener('click', handler); modal.show(); } // ===== TOGGLE ===== async function toggleShare(type, identifier, enable) { let res; if (type === 'smb') { res = await apiFetch(`/api/share/smb/${encodeURIComponent(identifier)}/toggle`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({enabled: enable}), }); } else { res = await apiFetch('/api/share/nfs/toggle', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({path: identifier, enabled: enable}), }); } if (!res) return; const data = await res.json(); if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger'); showToast(`Share ${enable ? 'enabled' : 'disabled'}.`); loadShares(); loadDashboard(); }