diff --git a/public/components/admin.html b/public/components/admin.html
index 01d2aff..41ee921 100644
--- a/public/components/admin.html
+++ b/public/components/admin.html
@@ -91,6 +91,48 @@
+
+
diff --git a/public/css/styles.css b/public/css/styles.css
index 057a837..fcc28f2 100644
--- a/public/css/styles.css
+++ b/public/css/styles.css
@@ -237,6 +237,11 @@ textarea.full-input{resize:vertical;}
.spinner{width:32px;height:32px;border:4px solid var(--g200);border-top-color:var(--blue);border-radius:50%;animation:spin 0.7s linear infinite;margin:0 auto 12px;}
@keyframes spin{to{transform:rotate(360deg);}}
.loading-box p{color:var(--g600);font-weight:500;font-size:13px;}
+.confirm-modal{position:fixed;inset:0;background:rgba(0,0,0,0.35);display:flex;align-items:center;justify-content:center;z-index:10000;backdrop-filter:blur(2px);}
+.confirm-modal.hidden{display:none;}
+.confirm-modal-box{background:white;padding:24px;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,0.15);max-width:420px;width:90%;animation:modalIn 0.15s ease;}
+.confirm-modal-box p{margin:0;font-size:14px;line-height:1.6;color:var(--g800);}
+@keyframes modalIn{from{opacity:0;transform:scale(0.95)} to{opacity:1;transform:scale(1)}}
/* FAQ */
.faq-container{max-width:800px;margin:0 auto;}
diff --git a/public/index.html b/public/index.html
index 3a39f7f..89eba52 100644
--- a/public/index.html
+++ b/public/index.html
@@ -280,6 +280,20 @@
+
+
diff --git a/public/js/admin.js b/public/js/admin.js
index 6d36d77..e621bd0 100644
--- a/public/js/admin.js
+++ b/public/js/admin.js
@@ -27,6 +27,7 @@
function loadAdmin() {
loadSettings();
loadUsers();
+ loadOidcConfig();
}
// ---- SETTINGS + STATS ----
@@ -162,17 +163,19 @@
switch (action) {
case 'verify':
- if (!confirm('Verify email for ' + email + '?')) return;
- adminPost('/api/admin/users/' + userId + '/verify', {}, function() {
- showToast(email + ' verified', 'success'); loadUsers();
+ showConfirm('Verify email for ' + email + '?', function() {
+ adminPost('/api/admin/users/' + userId + '/verify', {}, function() {
+ showToast(email + ' verified', 'success'); loadUsers();
+ });
});
break;
case 'disable':
- if (!confirm('Disable account for ' + email + '?')) return;
- adminPost('/api/admin/users/' + userId + '/disable', {}, function() {
- showToast(email + ' disabled', 'info'); loadUsers();
- });
+ showConfirm('Disable account for ' + email + '?', function() {
+ adminPost('/api/admin/users/' + userId + '/disable', {}, function() {
+ showToast(email + ' disabled', 'info'); loadUsers();
+ });
+ }, { danger: true, confirmText: 'Disable' });
break;
case 'enable':
@@ -182,66 +185,56 @@
break;
case 'promote':
- if (!confirm('Grant admin role to ' + email + '?')) return;
- adminPost('/api/admin/users/' + userId + '/role', { role: 'admin' }, function() {
- showToast(email + ' is now admin', 'success'); loadUsers();
+ showConfirm('Grant admin role to ' + email + '?', function() {
+ adminPost('/api/admin/users/' + userId + '/role', { role: 'admin' }, function() {
+ showToast(email + ' is now admin', 'success'); loadUsers();
+ });
});
break;
case 'demote':
- if (!confirm('Remove admin role from ' + email + '?')) return;
- adminPost('/api/admin/users/' + userId + '/role', { role: 'user' }, function() {
- showToast(email + ' demoted to user', 'info'); loadUsers();
+ showConfirm('Remove admin role from ' + email + '?', function() {
+ adminPost('/api/admin/users/' + userId + '/role', { role: 'user' }, function() {
+ showToast(email + ' demoted to user', 'info'); loadUsers();
+ });
});
break;
case 'set-moderator':
- if (!confirm('Set ' + email + ' as moderator? They will be able to manage Learning Hub content.')) return;
- adminPost('/api/admin/users/' + userId + '/role', { role: 'moderator' }, function() {
- showToast(email + ' is now a moderator', 'success'); loadUsers();
+ showConfirm('Set ' + email + ' as moderator? They will be able to manage Learning Hub content.', function() {
+ adminPost('/api/admin/users/' + userId + '/role', { role: 'moderator' }, function() {
+ showToast(email + ' is now a moderator', 'success'); loadUsers();
+ });
});
break;
case 'set-user':
- if (!confirm('Set ' + email + ' to full user role?')) return;
- adminPost('/api/admin/users/' + userId + '/role', { role: 'user' }, function() {
- showToast(email + ' is now a user', 'success'); loadUsers();
+ showConfirm('Set ' + email + ' to regular user role?', function() {
+ adminPost('/api/admin/users/' + userId + '/role', { role: 'user' }, function() {
+ showToast(email + ' is now a user', 'success'); loadUsers();
+ });
});
break;
case 'reset-pw':
- // Show inline password input instead of prompt()
- var existingResetRow = document.getElementById('admin-reset-pw-row');
- if (existingResetRow) existingResetRow.remove();
- var actionCell = btn.closest('td') || btn.parentElement;
- var resetRow = document.createElement('div');
- resetRow.id = 'admin-reset-pw-row';
- resetRow.style.cssText = 'margin-top:8px;display:flex;gap:6px;align-items:center;';
- resetRow.innerHTML = '
'
- + '
'
- + '
';
- actionCell.appendChild(resetRow);
- document.getElementById('admin-reset-pw-input').focus();
- document.getElementById('admin-reset-pw-submit').onclick = function() {
- var val = document.getElementById('admin-reset-pw-input').value;
- if (!val || val.length < 8) { showToast('Password must be 8+ characters', 'error'); return; }
+ showConfirm('New password for ' + email + ' (8+ characters):', function(val) {
+ if (val.length < 8) { showToast('Password must be 8+ characters', 'error'); return; }
adminPost('/api/admin/users/' + userId + '/reset-password', { newPassword: val }, function() {
showToast('Password reset for ' + email, 'success');
- resetRow.remove();
});
- };
- document.getElementById('admin-reset-pw-cancel').onclick = function() { resetRow.remove(); };
+ }, { input: true, inputType: 'password', placeholder: 'New password', required: true, requiredMsg: 'Enter a password', confirmText: 'Reset Password' });
break;
case 'delete':
- if (!confirm('Permanently delete user ' + (name || email) + '?\n\nThis cannot be undone.')) return;
- fetch('/api/admin/users/' + userId, { method: 'DELETE', headers: getAuthHeaders() })
- .then(function(r) { return r.json(); })
- .then(function(data) {
- if (data.success) { showToast(email + ' deleted', 'info'); loadUsers(); }
- else showToast(data.error || 'Delete failed', 'error');
- })
- .catch(function() { showToast('Request failed', 'error'); });
+ showConfirm('Permanently delete user ' + (name || email) + '? This cannot be undone.', function() {
+ fetch('/api/admin/users/' + userId, { method: 'DELETE', headers: getAuthHeaders() })
+ .then(function(r) { return r.json(); })
+ .then(function(data) {
+ if (data.success) { showToast(email + ' deleted', 'info'); loadUsers(); }
+ else showToast(data.error || 'Delete failed', 'error');
+ })
+ .catch(function() { showToast('Request failed', 'error'); });
+ }, { danger: true, confirmText: 'Delete' });
break;
}
}
@@ -435,6 +428,66 @@
// ---- AI PROMPTS ----
+ // ---- OIDC / SSO CONFIG ----
+ function loadOidcConfig() {
+ // Set callback URL display
+ var callbackEl = document.getElementById('oidc-callback-url');
+ if (callbackEl) callbackEl.textContent = window.location.origin + '/api/auth/oidc/callback';
+
+ fetch('/api/auth/oidc/config', { headers: getAuthHeaders() })
+ .then(function(r) { return r.json(); })
+ .then(function(data) {
+ if (!data.success) return;
+ var c = data.config || {};
+ var el;
+ el = document.getElementById('oidc-enabled'); if (el) el.value = c['oidc.enabled'] || 'false';
+ el = document.getElementById('oidc-issuer'); if (el) el.value = c['oidc.issuer'] || '';
+ el = document.getElementById('oidc-client-id'); if (el) el.value = c['oidc.client_id'] || '';
+ el = document.getElementById('oidc-client-secret'); if (el) el.placeholder = c['oidc.client_secret'] ? c['oidc.client_secret'] : 'Enter client secret';
+ el = document.getElementById('oidc-button-label'); if (el) el.value = c['oidc.button_label'] || '';
+ el = document.getElementById('oidc-disable-local'); if (el) el.value = c['oidc.disable_local_auth'] || 'false';
+ })
+ .catch(function() {});
+ }
+
+ document.addEventListener('click', function(e) {
+ if (e.target.id === 'btn-save-oidc' || e.target.closest('#btn-save-oidc')) {
+ e.preventDefault();
+ var status = document.getElementById('oidc-save-status');
+ if (status) { status.textContent = 'Saving...'; status.style.color = 'var(--g500)'; }
+
+ var body = {
+ 'oidc.enabled': document.getElementById('oidc-enabled').value,
+ 'oidc.issuer': document.getElementById('oidc-issuer').value.trim(),
+ 'oidc.client_id': document.getElementById('oidc-client-id').value.trim(),
+ 'oidc.button_label': document.getElementById('oidc-button-label').value.trim(),
+ 'oidc.disable_local_auth': document.getElementById('oidc-disable-local').value
+ };
+ // Only send secret if user typed a new one
+ var secretEl = document.getElementById('oidc-client-secret');
+ if (secretEl && secretEl.value) body['oidc.client_secret'] = secretEl.value;
+
+ fetch('/api/auth/oidc/config', {
+ method: 'PUT',
+ headers: getAuthHeaders(),
+ body: JSON.stringify(body)
+ })
+ .then(function(r) { return r.json(); })
+ .then(function(data) {
+ if (data.success) {
+ showToast('OIDC settings saved', 'success');
+ if (status) status.textContent = '';
+ if (secretEl) secretEl.value = '';
+ loadOidcConfig();
+ } else {
+ showToast(data.error || 'Save failed', 'error');
+ if (status) status.textContent = '';
+ }
+ })
+ .catch(function() { showToast('Save failed', 'error'); if (status) status.textContent = ''; });
+ }
+ });
+
function loadPromptList() {
fetch('/api/admin/config/prompts', { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })
@@ -485,25 +538,25 @@
var sel = document.getElementById('cms-prompt-select');
if (!sel || !sel.value) return;
var key = sel.value;
- if (!confirm('Reset "' + key + '" to the hardcoded default? This cannot be undone.')) return;
-
- fetch('/api/admin/config/prompts/' + key + '/reset', {
- method: 'POST',
- headers: getAuthHeaders()
- })
- .then(function(r) { return r.json(); })
- .then(function(data) {
- if (data.success) {
- if (!window._adminPrompts) window._adminPrompts = {};
- window._adminPrompts[key] = data.value;
- var textarea = document.getElementById('cms-prompt-text');
- if (textarea) textarea.value = data.value;
- showToast('Prompt reset to default', 'success');
- } else {
- showToast(data.error || 'Reset failed', 'error');
- }
- })
- .catch(function() { showToast('Request failed', 'error'); });
+ showConfirm('Reset "' + key + '" to the hardcoded default? This cannot be undone.', function() {
+ fetch('/api/admin/config/prompts/' + key + '/reset', {
+ method: 'POST',
+ headers: getAuthHeaders()
+ })
+ .then(function(r) { return r.json(); })
+ .then(function(data) {
+ if (data.success) {
+ if (!window._adminPrompts) window._adminPrompts = {};
+ window._adminPrompts[key] = data.value;
+ var textarea = document.getElementById('cms-prompt-text');
+ if (textarea) textarea.value = data.value;
+ showToast('Prompt reset to default', 'success');
+ } else {
+ showToast(data.error || 'Reset failed', 'error');
+ }
+ })
+ .catch(function() { showToast('Request failed', 'error'); });
+ });
}
// ---- SMTP ----
@@ -550,14 +603,15 @@
}
function clearSmtp() {
- if (!confirm('Clear DB SMTP settings? Env vars will be used if set.')) return;
- fetch('/api/admin/config/smtp', { method: 'DELETE', headers: getAuthHeaders() })
- .then(function(r) { return r.json(); })
- .then(function(data) {
- if (data.success) { showToast('SMTP DB settings cleared', 'info'); loadSmtp(); }
- else showToast(data.error || 'Failed', 'error');
- })
- .catch(function() { showToast('Request failed', 'error'); });
+ showConfirm('Clear DB SMTP settings? Env vars will be used if set.', function() {
+ fetch('/api/admin/config/smtp', { method: 'DELETE', headers: getAuthHeaders() })
+ .then(function(r) { return r.json(); })
+ .then(function(data) {
+ if (data.success) { showToast('SMTP DB settings cleared', 'info'); loadSmtp(); }
+ else showToast(data.error || 'Failed', 'error');
+ })
+ .catch(function() { showToast('Request failed', 'error'); });
+ });
}
// ---- AUTO-DELETE / RESET ----
@@ -574,17 +628,18 @@
}
function resetAllDefaults() {
- if (!confirm('Reset all settings to factory defaults?\n\nAnnouncements, feature flags, email templates will be reset. SMTP and custom models are preserved.')) return;
- fetch('/api/admin/config/reset-defaults', { method: 'POST', headers: getAuthHeaders() })
- .then(function(r) { return r.json(); })
- .then(function(data) {
- if (data.success) {
- showToast(data.message || 'Reset to defaults', 'success');
- cmsLoaded = false;
- loadCms();
- } else showToast(data.error || 'Reset failed', 'error');
- })
- .catch(function() { showToast('Request failed', 'error'); });
+ showConfirm('Reset all settings to factory defaults? Announcements, feature flags, email templates will be reset. SMTP and custom models are preserved.', function() {
+ fetch('/api/admin/config/reset-defaults', { method: 'POST', headers: getAuthHeaders() })
+ .then(function(r) { return r.json(); })
+ .then(function(data) {
+ if (data.success) {
+ showToast(data.message || 'Reset to defaults', 'success');
+ cmsLoaded = false;
+ loadCms();
+ } else showToast(data.error || 'Reset failed', 'error');
+ })
+ .catch(function() { showToast('Request failed', 'error'); });
+ }, { danger: true, confirmText: 'Reset' });
}
// ---- HELPERS ----
@@ -887,18 +942,19 @@
}
function clearAllModels() {
- if (!confirm('Remove all added models? You will need to re-add them via Search API.')) return;
- Promise.all([
- fetch('/api/admin/config/models/clear-all', { method: 'POST', headers: getAuthHeaders() })
- .then(function(r) { return r.json(); })
- ]).then(function(results) {
- if (results[0].success) {
- showToast('All models cleared', 'info');
- loadAdminModels();
- } else {
- showToast(results[0].error || 'Failed', 'error');
- }
- }).catch(function() { showToast('Request failed', 'error'); });
+ showConfirm('Remove all added models? You will need to re-add them via Search API.', function() {
+ Promise.all([
+ fetch('/api/admin/config/models/clear-all', { method: 'POST', headers: getAuthHeaders() })
+ .then(function(r) { return r.json(); })
+ ]).then(function(results) {
+ if (results[0].success) {
+ showToast('All models cleared', 'info');
+ loadAdminModels();
+ } else {
+ showToast(results[0].error || 'Failed', 'error');
+ }
+ }).catch(function() { showToast('Request failed', 'error'); });
+ });
}
function saveDefaultModel() {
diff --git a/public/js/adminMilestones.js b/public/js/adminMilestones.js
index 3a6e63d..d3b98c4 100644
--- a/public/js/adminMilestones.js
+++ b/public/js/adminMilestones.js
@@ -30,9 +30,9 @@
document.getElementById('btn-save-milestone')?.addEventListener('click', saveMilestone);
document.getElementById('btn-bulk-import-milestones')?.addEventListener('click', bulkImportMilestones);
document.getElementById('btn-reimport-milestones')?.addEventListener('click', function() {
- if (confirm('This will DELETE all existing milestones and re-import from static data. Continue?')) {
+ showConfirm('This will DELETE all existing milestones and re-import from static data. Continue?', function() {
bulkImportMilestones(true);
- }
+ }, { danger: true, confirmText: 'Re-import' });
});
// Filter
@@ -239,26 +239,26 @@
};
window.deleteMilestone = function(id) {
- if (!confirm('Delete this milestone? This action cannot be undone.')) return;
-
- fetch('/api/admin/milestones/' + id, {
- method: 'DELETE',
- headers: getAuthHeaders()
- })
- .then(r => r.json())
- .then(data => {
- if (!data.success) {
- showToast(data.error || 'Failed to delete', 'error');
- return;
- }
- showToast('Milestone deleted', 'success');
- loadMetadata();
- loadMilestones();
- })
- .catch(err => {
- console.error('[AdminMilestones] Delete error:', err);
- showToast('Failed to delete: ' + err.message, 'error');
- });
+ showConfirm('Delete this milestone? This action cannot be undone.', function() {
+ fetch('/api/admin/milestones/' + id, {
+ method: 'DELETE',
+ headers: getAuthHeaders()
+ })
+ .then(r => r.json())
+ .then(data => {
+ if (!data.success) {
+ showToast(data.error || 'Failed to delete', 'error');
+ return;
+ }
+ showToast('Milestone deleted', 'success');
+ loadMetadata();
+ loadMilestones();
+ })
+ .catch(err => {
+ console.error('[AdminMilestones] Delete error:', err);
+ showToast('Failed to delete: ' + err.message, 'error');
+ });
+ }, { danger: true, confirmText: 'Delete' });
};
function bulkImportMilestones(clearExisting) {
diff --git a/public/js/app.js b/public/js/app.js
index a1bcf6b..ba9fc42 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -440,6 +440,61 @@ function hideBusy() {
if (bar) bar.classList.remove('active');
}
+// Reusable confirmation modal — replaces browser confirm() and prompt()
+// Usage: showConfirm('Are you sure?', function() { doSomething(); });
+// With input: showConfirm('Enter email:', function(value) { send(value); }, { input: true, placeholder: 'email@example.com', inputType: 'email' });
+// With danger styling: showConfirm('Delete user?', function() { deleteUser(); }, { danger: true, confirmText: 'Delete' });
+window.showConfirm = function(message, onConfirm, opts) {
+ opts = opts || {};
+ var modal = document.getElementById('confirm-modal');
+ var text = document.getElementById('confirm-modal-text');
+ var okBtn = document.getElementById('confirm-modal-ok');
+ var cancelBtn = document.getElementById('confirm-modal-cancel');
+ var inputWrap = document.getElementById('confirm-modal-input-wrap');
+ var inputEl = document.getElementById('confirm-modal-input');
+ if (!modal || !text || !okBtn) return;
+
+ text.textContent = message;
+ okBtn.textContent = opts.confirmText || 'Confirm';
+ if (opts.danger) { okBtn.style.background = 'var(--red)'; okBtn.style.borderColor = 'var(--red)'; }
+ else { okBtn.style.background = ''; okBtn.style.borderColor = ''; }
+
+ if (opts.input) {
+ inputWrap.style.display = 'block';
+ inputEl.type = opts.inputType || 'text';
+ inputEl.placeholder = opts.placeholder || '';
+ inputEl.value = opts.defaultValue || '';
+ setTimeout(function() { inputEl.focus(); }, 50);
+ } else {
+ inputWrap.style.display = 'none';
+ }
+
+ modal.classList.remove('hidden');
+
+ function cleanup() {
+ modal.classList.add('hidden');
+ okBtn.onclick = null;
+ cancelBtn.onclick = null;
+ inputEl.value = '';
+ }
+
+ cancelBtn.onclick = function() { cleanup(); };
+ okBtn.onclick = function() {
+ if (opts.input) {
+ var val = inputEl.value;
+ if (opts.required && !val.trim()) { showToast(opts.requiredMsg || 'This field is required', 'error'); return; }
+ cleanup();
+ if (onConfirm) onConfirm(val);
+ } else {
+ cleanup();
+ if (onConfirm) onConfirm();
+ }
+ };
+
+ // Allow Enter key to confirm
+ inputEl.onkeydown = function(e) { if (e.key === 'Enter') okBtn.click(); };
+};
+
function showToast(message, type) {
var container = document.getElementById('toast-container');
if (!container) { console.log('Toast:', type, message); return; }
diff --git a/public/js/auth.js b/public/js/auth.js
index fc97b82..65c59e2 100644
--- a/public/js/auth.js
+++ b/public/js/auth.js
@@ -560,13 +560,14 @@ document.addEventListener('DOMContentLoaded', function() {
// Wire revoke buttons
list.querySelectorAll('.session-revoke-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
- if (!confirm('Revoke this session? That device will be logged out.')) return;
- fetch('/api/sessions/' + btn.dataset.sid, { method: 'DELETE', headers: getAuthHeaders() })
- .then(function(r) { return r.json(); })
- .then(function(d) {
- if (d.success) { showToast('Session revoked', 'info'); loadSessions(); }
- else showToast(d.error || 'Failed', 'error');
- });
+ showConfirm('Revoke this session? That device will be logged out.', function() {
+ fetch('/api/sessions/' + btn.dataset.sid, { method: 'DELETE', headers: getAuthHeaders() })
+ .then(function(r) { return r.json(); })
+ .then(function(d) {
+ if (d.success) { showToast('Session revoked', 'info'); loadSessions(); }
+ else showToast(d.error || 'Failed', 'error');
+ });
+ });
});
});
})
@@ -579,12 +580,13 @@ document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('click', function(e) {
if (e.target.id === 'btn-revoke-all-sessions' || e.target.closest('#btn-revoke-all-sessions')) {
e.preventDefault();
- if (!confirm('Revoke all other sessions? All other devices will be logged out.')) return;
- fetch('/api/sessions', { method: 'DELETE', headers: getAuthHeaders() })
- .then(function(r) { return r.json(); })
- .then(function(d) {
- if (d.success) { showToast('All other sessions revoked (' + (d.revoked || 0) + ' removed)', 'info'); loadSessions(); }
- });
+ showConfirm('Revoke all other sessions? All other devices will be logged out.', function() {
+ fetch('/api/sessions', { method: 'DELETE', headers: getAuthHeaders() })
+ .then(function(r) { return r.json(); })
+ .then(function(d) {
+ if (d.success) { showToast('All other sessions revoked (' + (d.revoked || 0) + ' removed)', 'info'); loadSessions(); }
+ });
+ }, { danger: true, confirmText: 'Revoke All' });
}
});
diff --git a/public/js/documents.js b/public/js/documents.js
index 90ccfb7..b46ca2b 100644
--- a/public/js/documents.js
+++ b/public/js/documents.js
@@ -96,14 +96,15 @@
}
function deleteDocument(id) {
- if (!confirm('Delete this document permanently?')) return;
- fetch('/api/documents/' + id, { method: 'DELETE', headers: getAuthHeaders() })
- .then(function(r) { return r.json(); })
- .then(function(data) {
- if (data.success) { showToast('Document deleted', 'info'); loadDocuments(); }
- else showToast(data.error || 'Delete failed', 'error');
- })
- .catch(function() { showToast('Delete failed', 'error'); });
+ showConfirm('Delete this document permanently?', function() {
+ fetch('/api/documents/' + id, { method: 'DELETE', headers: getAuthHeaders() })
+ .then(function(r) { return r.json(); })
+ .then(function(data) {
+ if (data.success) { showToast('Document deleted', 'info'); loadDocuments(); }
+ else showToast(data.error || 'Delete failed', 'error');
+ })
+ .catch(function() { showToast('Delete failed', 'error'); });
+ }, { danger: true, confirmText: 'Delete' });
}
function esc(str) {
diff --git a/public/js/memories.js b/public/js/memories.js
index 0f5b056..f4643c3 100644
--- a/public/js/memories.js
+++ b/public/js/memories.js
@@ -214,14 +214,15 @@
function deleteMemory(id) {
var mem = _memories.find(function(m) { return m.id === id; });
- if (!confirm('Delete template "' + (mem ? mem.name : '') + '"?')) return;
- fetch('/api/memories/' + id, { method: 'DELETE', headers: getAuthHeaders() })
- .then(function(r) { return r.json(); })
- .then(function(data) {
- if (data.success) { showToast('Template deleted', 'info'); loadMemories(); }
- else showToast(data.error || 'Delete failed', 'error');
- })
- .catch(function() { showToast('Delete failed', 'error'); });
+ showConfirm('Delete template "' + (mem ? mem.name : '') + '"?', function() {
+ fetch('/api/memories/' + id, { method: 'DELETE', headers: getAuthHeaders() })
+ .then(function(r) { return r.json(); })
+ .then(function(data) {
+ if (data.success) { showToast('Template deleted', 'info'); loadMemories(); }
+ else showToast(data.error || 'Delete failed', 'error');
+ })
+ .catch(function() { showToast('Delete failed', 'error'); });
+ }, { danger: true, confirmText: 'Delete' });
}
// ── Event wiring ─────────────────────────────────────────────────────────
diff --git a/public/js/transcriptionSettings.js b/public/js/transcriptionSettings.js
index 105f465..5d920c0 100644
--- a/public/js/transcriptionSettings.js
+++ b/public/js/transcriptionSettings.js
@@ -126,28 +126,31 @@
speechCheckbox.addEventListener('change', function() {
if (speechCheckbox.checked) {
// Show confirmation for privacy warning
- if (!confirm(
+ showConfirm(
'WARNING: Real-time streaming uses your browser\'s speech recognition, ' +
- 'which may send audio to cloud servers (e.g., Google).\n\n' +
- 'This is NOT HIPAA-compliant.\n\n' +
- 'Only enable if you understand and accept this privacy trade-off.\n\n' +
- 'Continue?'
- )) {
- speechCheckbox.checked = false;
- return;
- }
-
- // Disable Browser Whisper if enabling Web Speech
- if (whisperCheckbox && whisperCheckbox.checked) {
- whisperCheckbox.checked = false;
- BrowserWhisper.setEnabled(false);
- whisperStatus.textContent = 'Off';
- showToast('Browser Whisper disabled (Web Speech takes priority)', 'info');
- }
+ 'which may send audio to cloud servers (e.g., Google). ' +
+ 'This is NOT HIPAA-compliant. ' +
+ 'Only enable if you understand and accept this privacy trade-off.',
+ function() {
+ // Disable Browser Whisper if enabling Web Speech
+ if (whisperCheckbox && whisperCheckbox.checked) {
+ whisperCheckbox.checked = false;
+ BrowserWhisper.setEnabled(false);
+ whisperStatus.textContent = 'Off';
+ showToast('Browser Whisper disabled (Web Speech takes priority)', 'info');
+ }
+ WebSpeechRecognition.setEnabled(true);
+ speechStatus.textContent = 'On — real-time streaming';
+ },
+ { danger: true, confirmText: 'Enable' }
+ );
+ // Revert checkbox until confirmed
+ speechCheckbox.checked = false;
+ return;
}
- WebSpeechRecognition.setEnabled(speechCheckbox.checked);
- speechStatus.textContent = speechCheckbox.checked ? 'On — real-time streaming' : 'Off';
+ WebSpeechRecognition.setEnabled(false);
+ speechStatus.textContent = 'Off';
});
// If not supported, disable and show message