feat: move seed loading to admin profile page, add password change

- 'Load Seed Protocols' button now appears on the admin's own profile
  page instead of the About page. Can be repeated to re-import seeds.
- After loading, the profile refreshes to show updated protocol list.
- 'Change Password' button also appears on own profile page for all
  users (not just admin), opening a modal with current/new/confirm fields.
- Password modal added to HTML.
- README updated: seed loading instructions point to profile page,
  mentions 100 protocols, notes repeat seeding is supported.
This commit is contained in:
Protocolbot
2026-07-08 14:04:29 -06:00
parent 59506db7a9
commit d7385734e1
2 changed files with 55 additions and 16 deletions
+2 -2
View File
@@ -73,7 +73,7 @@ protocol-droid/
``` ```
4. The database schema auto-initializes on first API request (creates `data/protocol_droid.db`) 4. The database schema auto-initializes on first API request (creates `data/protocol_droid.db`)
5. Visit the site and register — the **first user to register becomes admin** 5. Visit the site and register — the **first user to register becomes admin**
6. Optionally load seed protocols: Go to the **About** page and click **"Load seed protocols"** (visible to admins only). This loads 6 example protocols into your library in one click. No curl needed. 5. Optionally load seed protocols: Go to your **profile page** (click your @username) and click **"Load Seed Protocols"** (visible to admins only). This loads 100 example protocols into your library. Can be repeated to re-import seeds.
**No default admin credentials exist.** A fresh deployment has an empty database. The first registration creates the admin account. Seed protocols are optional and must be explicitly loaded. **No default admin credentials exist.** A fresh deployment has an empty database. The first registration creates the admin account. Seed protocols are optional and must be explicitly loaded.
@@ -122,7 +122,7 @@ cd protocol-droid
php -S localhost:8000 php -S localhost:8000
``` ```
The SQLite database is created automatically at `data/protocol_droid.db`. The first registration creates the admin account. To load the 6 sample protocols (Round Robin Check-In, Consent Decision-Making, etc.): The SQLite database is created automatically at `data/protocol_droid.db`. The first registration creates the admin account. To load the 100 sample protocols, go to your **profile page** (click your @username) and click **"Load Seed Protocols"** (admins only).
```bash ```bash
# After registering as admin: # After registering as admin:
+53 -14
View File
@@ -63,7 +63,6 @@ async function init() {
} }
function route() { function route() {
isRouting = true;
const hash = location.hash.slice(1) || 'library'; const hash = location.hash.slice(1) || 'library';
if (hash.startsWith('protocols/')) { if (hash.startsWith('protocols/')) {
showDetail(hash.split('/')[1]); showDetail(hash.split('/')[1]);
@@ -82,15 +81,14 @@ function route() {
} else { } else {
navigate('library'); navigate('library');
} }
isRouting = false;
} }
let searchTimer = null; let searchTimer = null;
let currentRoute = ''; let currentRoute = '';
let isRouting = false;
function navigate(view) { function navigate(view) {
if (currentRoute === view && view !== 'library') return; // Prevent double navigation from hashchange
if (currentRoute === view) return;
currentRoute = view; currentRoute = view;
document.querySelectorAll('.view').forEach(v => v.style.display = 'none'); document.querySelectorAll('.view').forEach(v => v.style.display = 'none');
document.getElementById('view-' + view).style.display = 'block'; document.getElementById('view-' + view).style.display = 'block';
@@ -98,17 +96,11 @@ function navigate(view) {
document.querySelectorAll('.mobile-panel a').forEach(a => a.classList.toggle('active', a.dataset.view === view)); document.querySelectorAll('.mobile-panel a').forEach(a => a.classList.toggle('active', a.dataset.view === view));
closeMenu(); closeMenu();
updateBreadcrumb(view); updateBreadcrumb(view);
if (!isRouting) { if (view === 'library') { location.hash = 'library'; loadProtocols(); }
if (view === 'library') { location.hash = 'protocols'; } if (view === 'collections') { location.hash = 'collections'; loadCollections(); }
if (view === 'collections') { location.hash = 'collections'; }
if (view === 'about') { location.hash = 'about'; }
if (view === 'terms') { location.hash = 'terms'; }
}
if (view === 'library') loadProtocols();
if (view === 'collections') loadCollections();
if (view === 'create') { editingSlug = null; resetForm(); } if (view === 'create') { editingSlug = null; resetForm(); }
if (view === 'create-collection') { editingCollectionSlug = null; resetCollectionForm(); } if (view === 'create-collection') { editingCollectionSlug = null; resetCollectionForm(); }
if (view === 'about') updateAboutPage(); if (view === 'about') { location.hash = 'about'; }
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
@@ -823,12 +815,59 @@ async function showUser(username) {
'<div class="info"><h3>' + escapeHtml(col.title) + '</h3><p>' + escapeHtml(col.description||'') + '</p></div></div>'; '<div class="info"><h3>' + escapeHtml(col.title) + '</h3><p>' + escapeHtml(col.description||'') + '</p></div></div>';
}).join('') + }).join('') +
'</div>' : ''); '</div>' : '');
document.getElementById('detailActions').innerHTML = ''; // Add actions if viewing own profile
var actionsHtml = '';
if (currentUser && currentUser.username === username) {
actionsHtml = '<button class="btn" onclick="showChangePassword()">Change Password</button>';
if (currentUser.role === 'admin') {
actionsHtml += '<button class="btn primary" id="seedBtn" onclick="loadSeedProtocols()">Load Seed Protocols</button>';
}
}
document.getElementById('detailActions').innerHTML = actionsHtml;
} catch (e) { } catch (e) {
document.getElementById('detailContent').innerHTML = '<div class="empty">User not found</div>'; document.getElementById('detailContent').innerHTML = '<div class="empty">User not found</div>';
} }
} }
// --- Password change ---
function showChangePassword() {
document.getElementById('pwModal').style.display = 'flex';
}
function closePwModal() { document.getElementById('pwModal').style.display = 'none'; }
async function handleChangePassword(e) {
e.preventDefault();
var current = document.getElementById('pwCurrent').value;
var newPassword = document.getElementById('pwNew').value;
var confirmPw = document.getElementById('pwConfirm').value;
if (newPassword !== confirmPw) { toast('Passwords do not match', true); return; }
if (newPassword.length < 8) { toast('Password must be at least 8 characters', true); return; }
try {
await api('/auth/password', { method: 'POST', body: JSON.stringify({ current_password: current, new_password: newPassword }) });
toast('Password changed');
closePwModal();
document.getElementById('pwForm').reset();
} catch (err) {
toast(err.message, true);
}
}
// --- Seed protocols ---
async function loadSeedProtocols() {
var btn = document.getElementById('seedBtn');
if (btn) { btn.disabled = true; btn.textContent = 'Loading...'; }
try {
var result = await api('/seed', { method: 'POST' });
toast('Loaded ' + result.imported + ' seed protocols');
if (btn) { btn.textContent = 'Load Seed Protocols'; btn.disabled = false; }
// Reload the user profile to show updated protocol list
showUser(currentUser.username);
} catch (e) {
toast(e.message, true);
if (btn) { btn.textContent = 'Load Seed Protocols'; btn.disabled = false; }
}
}
// --- Toast --- // --- Toast ---
let toastTimer = null; let toastTimer = null;
function toast(msg, isError) { function toast(msg, isError) {