phase 8 server-side validation (configSchema, inline field errors, partial-success semantics)
This commit is contained in:
@@ -970,3 +970,22 @@ button.ss-chip {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* ---------- Field-level validation errors (Phase 8) ---------- */
|
||||
|
||||
.field.field-error input,
|
||||
.field.field-error select,
|
||||
.field.field-error textarea,
|
||||
.field.field-error .smart-select-display {
|
||||
border-color: var(--danger);
|
||||
box-shadow: 0 0 0 3px rgba(255, 90, 82, 0.15);
|
||||
}
|
||||
.field.field-error label { color: var(--danger); }
|
||||
.field-error-message {
|
||||
font-family: var(--font-body);
|
||||
font-size: 12px;
|
||||
color: var(--danger);
|
||||
margin-top: 2px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -80,9 +80,46 @@
|
||||
updateSaveBar();
|
||||
}
|
||||
|
||||
function clearFieldErrors() {
|
||||
document.querySelectorAll('.field.field-error').forEach(f => f.classList.remove('field-error'));
|
||||
document.querySelectorAll('.field-error-message').forEach(el => el.remove());
|
||||
}
|
||||
|
||||
function normalizeErrorEntry(e) {
|
||||
if (e && typeof e === 'object' && 'key' in e) {
|
||||
return { key: e.key, message: e.error || 'Invalid value' };
|
||||
}
|
||||
// Tolerate legacy string shape "KEY: message" from older bot builds.
|
||||
const str = String(e);
|
||||
const idx = str.indexOf(':');
|
||||
if (idx > 0) return { key: str.slice(0, idx).trim(), message: str.slice(idx + 1).trim() };
|
||||
return { key: str, message: 'Invalid value' };
|
||||
}
|
||||
|
||||
function applyFieldErrors(errors) {
|
||||
let firstField = null;
|
||||
for (const raw of errors) {
|
||||
const { key, message } = normalizeErrorEntry(raw);
|
||||
const selector = `[data-key="${(window.CSS && CSS.escape) ? CSS.escape(key) : key}"]`;
|
||||
const input = document.querySelector(selector);
|
||||
if (!input) continue;
|
||||
const field = input.closest('.field');
|
||||
if (!field) continue;
|
||||
field.classList.add('field-error');
|
||||
const msgEl = document.createElement('div');
|
||||
msgEl.className = 'field-error-message';
|
||||
msgEl.textContent = message;
|
||||
msgEl.setAttribute('role', 'alert');
|
||||
field.appendChild(msgEl);
|
||||
if (!firstField) firstField = field;
|
||||
}
|
||||
if (firstField) firstField.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
async function saveConfig(mode) {
|
||||
const buttons = document.querySelectorAll('#save-bar button');
|
||||
buttons.forEach(b => b.disabled = true);
|
||||
clearFieldErrors();
|
||||
try {
|
||||
if (mode === 'restart' && !confirm('Save changes and restart the bot now?')) {
|
||||
return;
|
||||
@@ -96,14 +133,21 @@
|
||||
const data = await res.json();
|
||||
if (data.applied) {
|
||||
for (const key of data.applied) savedConfig[key] = pendingChanges[key];
|
||||
pendingChanges = {};
|
||||
for (const key of data.applied) delete pendingChanges[key];
|
||||
updateSaveBar();
|
||||
document.querySelectorAll('.changed').forEach(el => el.classList.remove('changed'));
|
||||
Util.showToast(`${data.applied.length} settings saved.`, 'success');
|
||||
document.querySelectorAll('.changed').forEach(el => {
|
||||
const key = el.dataset && el.dataset.key;
|
||||
if (!key || !(key in pendingChanges)) el.classList.remove('changed');
|
||||
});
|
||||
if (data.applied.length > 0) {
|
||||
Util.showToast(`${data.applied.length} setting${data.applied.length === 1 ? '' : 's'} saved.`, 'success');
|
||||
}
|
||||
}
|
||||
const hasErrors = data.errors && data.errors.length > 0;
|
||||
if (hasErrors) {
|
||||
Util.showToast(`Errors: ${data.errors.join(', ')}`, 'error');
|
||||
applyFieldErrors(data.errors);
|
||||
const keys = data.errors.map(e => normalizeErrorEntry(e).key).join(', ');
|
||||
Util.showToast(`${data.errors.length} setting${data.errors.length === 1 ? '' : 's'} failed: ${keys}`, 'error');
|
||||
}
|
||||
if (mode === 'restart' && !hasErrors) {
|
||||
await fetch('/api/restart', {
|
||||
@@ -114,7 +158,7 @@
|
||||
});
|
||||
Util.showToast('Restart initiated.', 'warning');
|
||||
} else if (mode === 'restart' && hasErrors) {
|
||||
Util.showToast(`Restart cancelled — save returned errors: ${data.errors.join(', ')}`, 'warning');
|
||||
Util.showToast('Restart cancelled — some settings failed validation.', 'warning');
|
||||
}
|
||||
} catch (e) {
|
||||
Util.showToast('Failed to save. Bot may be unreachable.', 'error');
|
||||
|
||||
Reference in New Issue
Block a user