diff --git a/static/js/main.js b/static/js/main.js
index 94ca235..b26e8db 100644
--- a/static/js/main.js
+++ b/static/js/main.js
@@ -1,4 +1,4 @@
loadDashboard();
loadShares();
loadUsers();
-setInterval(loadDashboard, 30_000);
+setInterval(() => { loadDashboard(); loadShares(); loadUsers(); }, 30_000);
diff --git a/static/js/shares.js b/static/js/shares.js
index 8bd1f76..6e6603a 100644
--- a/static/js/shares.js
+++ b/static/js/shares.js
@@ -9,21 +9,26 @@ async function loadShares() {
} else {
sb.innerHTML = shares.smb.map(s => {
const enabled = s.enabled !== false;
- const statusDot = ``;
+ 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));
+ const pathJson = esc(JSON.stringify(s.path));
+ const usersJson = esc(JSON.stringify(s.valid_users));
return `
| ${esc(s.name)} |
${esc(s.path)} |
${esc(s.valid_users || '—')} |
- ${statusDot} |
+ ${dot} |
+ onclick="openEditModal('smb',${shareJson})">Edit
+ onclick="openQuotaModal('smb',${nameJson},${pathJson},${usersJson})">Quota
+ onclick="toggleShare('smb',${nameJson},${!enabled})">${enabled ? 'Disable' : 'Enable'}
+ onclick="deleteShare('smb',${nameJson})">Delete
|
`;
}).join('');
@@ -35,20 +40,22 @@ async function loadShares() {
} else {
nb.innerHTML = shares.nfs.map(s => {
const enabled = s.enabled !== false;
- const statusDot = ``;
+ const dot = ``;
+ const shareJson = esc(JSON.stringify(s));
+ const pathJson = esc(JSON.stringify(s.path));
return `
${esc(s.path)} |
${esc(s.clients)} |
- ${statusDot} |
+ ${dot} |
+ onclick="openEditModal('nfs',${shareJson})">Edit
+ onclick="openQuotaModal('nfs',null,${pathJson},null)">Quota
+ onclick="toggleShare('nfs',${pathJson},${!enabled})">${enabled ? 'Disable' : 'Enable'}
+ onclick="deleteShare('nfs',${pathJson})">Delete
|
`;
}).join('');
@@ -173,25 +180,33 @@ async function submitEdit() {
// ===== DELETE =====
-async function deleteShare(type, identifier) {
+function deleteShare(type, identifier) {
const label = type === 'smb' ? `SMB share "${identifier}"` : `NFS export "${identifier}"`;
- if (!confirm(`Delete ${label}? This cannot be undone.`)) return;
+ document.getElementById('deleteModalMsg').textContent =
+ `Are you sure you want to delete ${label}? This cannot be undone.`;
- 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();
+ 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 =====
diff --git a/static/js/users.js b/static/js/users.js
index ed8a9f2..8d30007 100644
--- a/static/js/users.js
+++ b/static/js/users.js
@@ -7,8 +7,10 @@ async function loadUsers() {
tb.innerHTML = `| No users found. |
`;
return;
}
- tb.innerHTML = users.map(u => `
-
+ tb.innerHTML = users.map(u => {
+ // esc() HTML-encodes JSON so " inside onclick="..." doesn't break the attribute
+ const nameJson = esc(JSON.stringify(u.name));
+ return `
| ${esc(u.name)} |
${u.uid} |
@@ -19,11 +21,12 @@ async function loadUsers() {
|
+ onclick="openPwModal(${nameJson})">Password
+ onclick="confirmDeleteUser(${nameJson})">Delete
|
-
`).join('');
+ `;
+ }).join('');
}
async function createUser() {
@@ -44,14 +47,24 @@ async function createUser() {
loadUsers();
}
-async function deleteUser(name) {
- if (!confirm(`Delete user "${name}"? This cannot be undone.`)) return;
- const res = await apiFetch(`/api/users/${encodeURIComponent(name)}`, { method: 'DELETE' });
- if (!res) return;
- const data = await res.json();
- if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger');
- showToast(`User "${name}" deleted.`);
- loadUsers();
+function confirmDeleteUser(name) {
+ document.getElementById('deleteModalMsg').textContent =
+ `Are you sure you want to delete user "${name}"? This removes both the system account and SMB access.`;
+
+ const modal = new bootstrap.Modal(document.getElementById('deleteModal'));
+ const btn = document.getElementById('deleteModalConfirm');
+ const handler = async () => {
+ btn.removeEventListener('click', handler);
+ modal.hide();
+ const res = await apiFetch(`/api/users/${encodeURIComponent(name)}`, { method: 'DELETE' });
+ if (!res) return;
+ const data = await res.json();
+ if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger');
+ showToast(`User "${name}" deleted.`);
+ loadUsers();
+ };
+ btn.addEventListener('click', handler);
+ modal.show();
}
function openPwModal(name) {
@@ -64,9 +77,9 @@ function openPwModal(name) {
async function submitPassword() {
const name = document.getElementById('pwUsername').value;
const pw = document.getElementById('pwNew').value;
- const confirm = document.getElementById('pwConfirm').value;
+ const pwConfirm = document.getElementById('pwConfirm').value;
if (pw.length < 8) return showToast('Password must be at least 8 characters.', 'danger');
- if (pw !== confirm) return showToast('Passwords do not match.', 'danger');
+ if (pw !== pwConfirm) return showToast('Passwords do not match.', 'danger');
const res = await apiFetch(`/api/users/${encodeURIComponent(name)}/password`, {
method: 'POST', headers: {'Content-Type': 'application/json'},
diff --git a/templates/index.html b/templates/index.html
index df1b649..289036b 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -65,6 +65,25 @@
+
+
+
MicroNAS