feat: add demo measurements to seed and user management to setup page

Seed now generates 180 realistic measurements (30 per subtask) with
Gaussian distribution for SPC testing. Setup page gains full user CRUD
with list, create/edit, password change, and activate/deactivate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Adriano
2026-02-24 14:49:58 +01:00
parent 272685e554
commit e07a4e4f89
3 changed files with 953 additions and 7 deletions
+388 -5
View File
@@ -224,6 +224,123 @@
.mt-4 { margin-top: 16px; }
.text-sm { font-size: 0.82rem; }
.text-muted { color: #94a3b8; }
/* --- User Management ----------------------------------------------- */
.user-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
margin-top: 12px;
}
.user-table th {
text-align: left;
padding: 8px 10px;
background: #f8fafc;
color: #475569;
font-weight: 600;
border-bottom: 2px solid #e2e8f0;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.user-table td {
padding: 8px 10px;
border-bottom: 1px solid #f1f5f9;
vertical-align: middle;
}
.user-table tr:hover td { background: #f8fafc; }
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.02em;
}
.badge-maker { background: #dbeafe; color: #1d4ed8; }
.badge-measurement { background: #dcfce7; color: #166534; }
.badge-metrologist { background: #fef3c7; color: #92400e; }
.badge-admin { background: #fce7f3; color: #9d174d; }
.badge-active { background: #dcfce7; color: #166534; }
.badge-inactive { background: #fee2e2; color: #991b1b; }
.btn-sm {
padding: 4px 10px;
font-size: 0.75rem;
border-radius: 6px;
border: none;
cursor: pointer;
font-weight: 500;
font-family: inherit;
transition: background .15s;
}
.btn-sm-primary { background: #2563eb; color: #fff; }
.btn-sm-primary:hover { background: #1d4ed8; }
.btn-sm-warning { background: #f59e0b; color: #fff; }
.btn-sm-warning:hover { background: #d97706; }
.btn-sm-danger { background: #ef4444; color: #fff; }
.btn-sm-danger:hover { background: #dc2626; }
.btn-sm-success { background: #10b981; color: #fff; }
.btn-sm-success:hover { background: #059669; }
.action-btns { display: flex; gap: 4px; flex-wrap: wrap; }
/* --- Modal --------------------------------------------------------- */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,.4);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
animation: fadeInOverlay .2s ease-out;
}
@keyframes fadeInOverlay { from { opacity: 0; } to { opacity: 1; } }
.modal {
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0,0,0,.2);
padding: 24px;
width: 90%;
max-width: 480px;
max-height: 90vh;
overflow-y: auto;
animation: scaleIn .2s ease-out;
}
@keyframes scaleIn { from { transform: scale(.95); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.modal h2 { margin-bottom: 16px; }
.modal .btn { margin-top: 8px; }
.modal-actions {
display: flex;
gap: 8px;
margin-top: 16px;
}
.modal-actions .btn { flex: 1; }
/* --- Checkbox group ------------------------------------------------ */
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 14px;
}
.checkbox-group label {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.85rem;
margin-bottom: 0;
cursor: pointer;
}
.checkbox-group input[type="checkbox"] {
width: 16px; height: 16px;
accent-color: #2563eb;
}
</style>
</head>
<body>
@@ -300,14 +417,26 @@
<div class="card">
<h2>Seed Demo Data</h2>
<p class="text-sm text-muted" style="margin-bottom:14px;">
Populate the database with sample users and a demo recipe.
Populate the database with sample users, a demo recipe, and 180 measurements for SPC testing.
Creates users: <span class="mono">admin</span>, <span class="mono">maker1</span>,
<span class="mono">tec1</span>, <span class="mono">metro1</span>.
</p>
<button class="btn btn-success" id="btn-seed" onclick="seedData()">Load Demo Data</button>
</div>
<!-- 5. Reset DB ---------------------------------------------------- -->
<!-- 5. User Management ------------------------------------------------ -->
<div class="card" id="card-users">
<h2>User Management</h2>
<p class="text-sm text-muted" style="margin-bottom:14px;">
Create, edit, and manage user accounts. Changes take effect immediately.
</p>
<button class="btn btn-primary" style="width:auto; margin-bottom:14px;" onclick="openCreateUserModal()">+ New User</button>
<div id="users-table-container">
<p class="text-sm text-muted">Unlock the setup page to load users.</p>
</div>
</div>
<!-- 6. Reset DB ---------------------------------------------------- -->
<div class="card danger">
<h2 style="color:#dc2626;">Reset Database</h2>
<p class="text-sm" style="color:#991b1b; margin-bottom:8px;">
@@ -321,6 +450,58 @@
</div>
</div>
<!-- ================================================================== -->
<!-- MODALS -->
<!-- ================================================================== -->
<!-- Create / Edit User Modal -->
<div class="modal-overlay hidden" id="modal-user">
<div class="modal">
<h2 id="modal-user-title">New User</h2>
<input type="hidden" id="mu-user-id">
<label for="mu-username">Username</label>
<input type="text" id="mu-username" placeholder="username">
<label for="mu-password">Password <span id="mu-pwd-hint" class="text-muted text-sm">(required)</span></label>
<input type="password" id="mu-password" placeholder="Password">
<label for="mu-displayname">Display Name</label>
<input type="text" id="mu-displayname" placeholder="Full Name">
<label for="mu-email">Email</label>
<input type="email" id="mu-email" placeholder="user@example.com">
<label>Roles</label>
<div class="checkbox-group">
<label><input type="checkbox" id="mu-role-maker" value="Maker"> Maker</label>
<label><input type="checkbox" id="mu-role-tec" value="MeasurementTec"> MeasurementTec</label>
<label><input type="checkbox" id="mu-role-metro" value="Metrologist"> Metrologist</label>
</div>
<div class="checkbox-group">
<label><input type="checkbox" id="mu-admin"> Administrator</label>
</div>
<div class="modal-actions">
<button class="btn btn-outline" onclick="closeModal('modal-user')">Cancel</button>
<button class="btn btn-primary" id="btn-save-user" onclick="saveUser()">Save User</button>
</div>
</div>
</div>
<!-- Change Password Modal -->
<div class="modal-overlay hidden" id="modal-password">
<div class="modal">
<h2>Change Password</h2>
<input type="hidden" id="mp-user-id">
<p class="text-sm text-muted" style="margin-bottom:14px;">
Changing password for: <strong id="mp-username"></strong>
</p>
<label for="mp-new-password">New Password</label>
<input type="password" id="mp-new-password" placeholder="New password">
<label for="mp-confirm-password">Confirm Password</label>
<input type="password" id="mp-confirm-password" placeholder="Confirm password">
<div class="modal-actions">
<button class="btn btn-outline" onclick="closeModal('modal-password')">Cancel</button>
<button class="btn btn-primary" onclick="changePassword()">Change Password</button>
</div>
</div>
</div>
<!-- ================================================================== -->
<!-- JAVASCRIPT -->
<!-- ================================================================== -->
@@ -394,7 +575,7 @@
+ 'Database initialized &mdash; ' + data.table_count + ' table(s)</div>';
html += '<div class="table-list">';
data.tables.forEach(function(t) {
html += '<span class="table-chip">' + t + '</span>';
html += '<span class="table-chip">' + escHtml(t) + '</span>';
});
html += '</div>';
} else {
@@ -404,7 +585,7 @@
container.innerHTML = html;
} catch (e) {
container.innerHTML = '<div class="status-bar err"><span class="dot red"></span>'
+ 'Could not fetch status: ' + e.message + '</div>';
+ 'Could not fetch status: ' + escHtml(e.message) + '</div>';
}
}
@@ -427,6 +608,7 @@
document.getElementById('login-screen').classList.add('hidden');
document.getElementById('setup-panel').classList.remove('hidden');
await checkStatus();
await loadUsers();
showToast('Setup panel unlocked', 'success');
} catch (e) {
showToast('Connection error: ' + e.message, 'error');
@@ -499,7 +681,11 @@
setLoading('btn-seed', true);
try {
const data = await apiPost('/seed', { password: setupPassword });
showToast(data.message + ' (users: ' + data.users_created.join(', ') + ')', 'success');
var msg = data.message + ' (users: ' + data.users_created.join(', ');
if (data.measurements_created) msg += ', measurements: ' + data.measurements_created;
msg += ')';
showToast(msg, 'success');
await loadUsers();
} catch (e) {
showToast(e.message, 'error');
} finally {
@@ -541,6 +727,203 @@
setLoading('btn-reset', false);
}
}
/* ================================================================== */
/* USER MANAGEMENT */
/* ================================================================== */
function escHtml(str) {
if (!str) return '';
var d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
function roleBadge(role) {
var cls = 'badge ';
if (role === 'Maker') cls += 'badge-maker';
else if (role === 'MeasurementTec') cls += 'badge-measurement';
else if (role === 'Metrologist') cls += 'badge-metrologist';
return '<span class="' + cls + '">' + escHtml(role) + '</span> ';
}
async function loadUsers() {
var container = document.getElementById('users-table-container');
try {
var data = await apiPost('/list-users', { password: setupPassword });
if (!data.users || data.users.length === 0) {
container.innerHTML = '<p class="text-sm text-muted">No users found. Use Seed Demo Data or create one.</p>';
return;
}
var html = '<table class="user-table"><thead><tr>'
+ '<th>ID</th><th>Username</th><th>Display Name</th>'
+ '<th>Roles</th><th>Status</th><th>Actions</th>'
+ '</tr></thead><tbody>';
data.users.forEach(function(u) {
var roles = '';
(u.roles || []).forEach(function(r) { roles += roleBadge(r); });
if (u.is_admin) roles += '<span class="badge badge-admin">Admin</span> ';
var statusBadge = u.active
? '<span class="badge badge-active">Active</span>'
: '<span class="badge badge-inactive">Inactive</span>';
var toggleLabel = u.active ? 'Deactivate' : 'Activate';
var toggleClass = u.active ? 'btn-sm-danger' : 'btn-sm-success';
html += '<tr>'
+ '<td class="mono">' + u.id + '</td>'
+ '<td class="mono">' + escHtml(u.username) + '</td>'
+ '<td>' + escHtml(u.display_name) + '</td>'
+ '<td>' + roles + '</td>'
+ '<td>' + statusBadge + '</td>'
+ '<td><div class="action-btns">'
+ '<button class="btn-sm btn-sm-primary" onclick="openEditUserModal(' + u.id + ')">Edit</button>'
+ '<button class="btn-sm btn-sm-warning" onclick="openPasswordModal(' + u.id + ', \'' + escHtml(u.username).replace(/'/g, "\\'") + '\')">Password</button>'
+ '<button class="btn-sm ' + toggleClass + '" onclick="toggleActive(' + u.id + ', \'' + escHtml(u.username).replace(/'/g, "\\'") + '\', ' + u.active + ')">' + toggleLabel + '</button>'
+ '</div></td></tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
} catch (e) {
container.innerHTML = '<div class="status-bar err"><span class="dot red"></span>' + escHtml(e.message) + '</div>';
}
}
/* -- Modal helpers ------------------------------------------------- */
function closeModal(id) {
document.getElementById(id).classList.add('hidden');
}
function openCreateUserModal() {
document.getElementById('modal-user-title').textContent = 'New User';
document.getElementById('mu-user-id').value = '';
document.getElementById('mu-username').value = '';
document.getElementById('mu-password').value = '';
document.getElementById('mu-displayname').value = '';
document.getElementById('mu-email').value = '';
document.getElementById('mu-role-maker').checked = false;
document.getElementById('mu-role-tec').checked = false;
document.getElementById('mu-role-metro').checked = false;
document.getElementById('mu-admin').checked = false;
document.getElementById('mu-pwd-hint').textContent = '(required)';
document.getElementById('modal-user').classList.remove('hidden');
}
/* Store user data for edit lookup */
var _usersCache = [];
async function openEditUserModal(userId) {
/* Reload users to get fresh data */
try {
var data = await apiPost('/list-users', { password: setupPassword });
_usersCache = data.users || [];
} catch (e) {
showToast('Failed to load user: ' + e.message, 'error');
return;
}
var user = _usersCache.find(function(u) { return u.id === userId; });
if (!user) { showToast('User not found', 'error'); return; }
document.getElementById('modal-user-title').textContent = 'Edit User';
document.getElementById('mu-user-id').value = user.id;
document.getElementById('mu-username').value = user.username;
document.getElementById('mu-password').value = '';
document.getElementById('mu-displayname').value = user.display_name;
document.getElementById('mu-email').value = user.email || '';
document.getElementById('mu-role-maker').checked = (user.roles || []).indexOf('Maker') !== -1;
document.getElementById('mu-role-tec').checked = (user.roles || []).indexOf('MeasurementTec') !== -1;
document.getElementById('mu-role-metro').checked = (user.roles || []).indexOf('Metrologist') !== -1;
document.getElementById('mu-admin').checked = user.is_admin;
document.getElementById('mu-pwd-hint').textContent = '(leave blank to keep current)';
document.getElementById('modal-user').classList.remove('hidden');
}
async function saveUser() {
var userId = document.getElementById('mu-user-id').value;
var username = document.getElementById('mu-username').value.trim();
var pwd = document.getElementById('mu-password').value;
var displayName = document.getElementById('mu-displayname').value.trim();
var email = document.getElementById('mu-email').value.trim();
if (!username || !displayName) {
showToast('Username and Display Name are required', 'error');
return;
}
if (!userId && !pwd) {
showToast('Password is required for new users', 'error');
return;
}
var roles = [];
if (document.getElementById('mu-role-maker').checked) roles.push('Maker');
if (document.getElementById('mu-role-tec').checked) roles.push('MeasurementTec');
if (document.getElementById('mu-role-metro').checked) roles.push('Metrologist');
setLoading('btn-save-user', true);
try {
var body = {
password: setupPassword,
user_id: userId ? parseInt(userId) : null,
username: username,
display_name: displayName,
email: email || null,
roles: roles,
is_admin: document.getElementById('mu-admin').checked,
};
if (pwd) body.user_password = pwd;
var data = await apiPost('/manage-user', body);
showToast(data.message, 'success');
closeModal('modal-user');
await loadUsers();
} catch (e) {
showToast(e.message, 'error');
} finally {
setLoading('btn-save-user', false);
}
}
function openPasswordModal(userId, username) {
document.getElementById('mp-user-id').value = userId;
document.getElementById('mp-username').textContent = username;
document.getElementById('mp-new-password').value = '';
document.getElementById('mp-confirm-password').value = '';
document.getElementById('modal-password').classList.remove('hidden');
}
async function changePassword() {
var userId = document.getElementById('mp-user-id').value;
var newPwd = document.getElementById('mp-new-password').value;
var confirmPwd = document.getElementById('mp-confirm-password').value;
if (!newPwd) { showToast('Password cannot be empty', 'error'); return; }
if (newPwd !== confirmPwd) { showToast('Passwords do not match', 'error'); return; }
try {
var data = await apiPost('/change-user-password', {
password: setupPassword,
user_id: parseInt(userId),
new_password: newPwd,
});
showToast(data.message, 'success');
closeModal('modal-password');
} catch (e) {
showToast(e.message, 'error');
}
}
async function toggleActive(userId, username, currentlyActive) {
var action = currentlyActive ? 'deactivate' : 'activate';
if (!confirm('Are you sure you want to ' + action + ' user "' + username + '"?')) return;
try {
var data = await apiPost('/toggle-user-active', {
password: setupPassword,
user_id: userId,
});
showToast(data.message, 'success');
await loadUsers();
} catch (e) {
showToast(e.message, 'error');
}
}
</script>
</body>
</html>