refactor(js): 4. Iteration – Card-Preview-Helper, Button-Loading, Stream-Events vereinfacht

Neue Helper:
- _renderCardPreview(): Gemeinsamer Helper für Agent/Session-Card-Vorschau
  (ersetzt renderAgentInCard + renderSessionSelectCard Duplikate)
- withButtonLoading(btn, text, fn): Eliminiert ~15x identisches
  disable/text/try/finally/reset Pattern in allen Pages
- formatDate(): Nach ui-helpers.js verschoben (war in dashboard.js)
- createMDE(): maxHeight-Option hinzugefügt

Dedupliziert:
- handleStreamEvent: 12 Preparer/Review/Finalizer cases → 7 durch
  case-Grouping (start/text/tool_use shared)
- _escCmdAttr() entfernt → nutzt jetzt esc() aus ui-helpers.js
- feInitEasyMDE() nutzt jetzt createMDE() statt eigener EasyMDE-Init

Betroffene Dateien: ui-helpers.js, chat.js, commands-ui.js,
file-explorer.js, dashboard.js, session-page.js, cronjob-page.js,
profile-page.js, admin-page.js, server-settings-page.js, settings-page.js
This commit is contained in:
Meleki committed 2026-03-07 22:34:37 +01:00
1 parent e4ccaf9dee
commit e930268476
11 files changed
+137 -196

No files matched your search

+5 -30
View File
@@ -73,16 +73,17 @@ function handleStreamEvent(eventType, data) {
addChoiceBubble(data.toolCallId, data.question, data.choices);
break;
case 'preparer_start':
// Preparer / Review / Finalizer events (shared pattern)
case 'preparer_start': case 'review_start': case 'finalizer_start':
endStreamBubble();
addReviewBubble('start', data, 'preparer');
addReviewBubble('start', data, eventType.split('_')[0] === 'review' ? undefined : eventType.split('_')[0]);
break;
case 'preparer_text':
case 'preparer_text': case 'review_text': case 'finalizer_text':
appendToReviewText(data.text);
break;
case 'preparer_tool_use':
case 'preparer_tool_use': case 'review_tool_use': case 'finalizer_tool_use':
addReviewToolUse(data);
break;
@@ -90,19 +91,6 @@ function handleStreamEvent(eventType, data) {
finalizeReviewBubble(data.forwarded ? 'forwarded' : 'query', data, 'preparer');
break;
case 'review_start':
endStreamBubble();
addReviewBubble('start', data);
break;
case 'review_text':
appendToReviewText(data.text);
break;
case 'review_tool_use':
addReviewToolUse(data);
break;
case 'review_feedback':
finalizeReviewBubble('feedback', data);
break;
@@ -111,19 +99,6 @@ function handleStreamEvent(eventType, data) {
finalizeReviewBubble('approved', data);
break;
case 'finalizer_start':
endStreamBubble();
addReviewBubble('start', data, 'finalizer');
break;
case 'finalizer_text':
appendToReviewText(data.text);
break;
case 'finalizer_tool_use':
addReviewToolUse(data);
break;
case 'finalizer_done':
finalizeReviewBubble('done', data, 'finalizer');
break;
+3 -5
View File
@@ -14,8 +14,8 @@ function createCommandRow(container, name, command, type) {
<option value="foreground">Vordergrund</option>
<option value="message">Nachricht</option>
</select>
<input type="text" class="cmd-name" placeholder="Name (z.B. Deploy)" value="${_escCmdAttr(name)}">
<input type="text" class="cmd-command" placeholder="${type === 'message' ? 'Nachricht...' : 'Befehl (z.B. ./deploy.sh)'}" value="${_escCmdAttr(command)}">
<input type="text" class="cmd-name" placeholder="Name (z.B. Deploy)" value="${esc(name)}">
<input type="text" class="cmd-command" placeholder="${type === 'message' ? 'Nachricht...' : 'Befehl (z.B. ./deploy.sh)'}" value="${esc(command)}">
</div>
<button type="button" class="remove-cmd-btn icon-btn"><i class="fa fa-times"></i></button>
`;
@@ -53,6 +53,4 @@ function loadCommandsIntoUI(container, customCommands) {
} catch (e) { /* ignore parse errors */ }
}
function _escCmdAttr(str) {
return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
+2 -16
View File
@@ -327,26 +327,13 @@ function feInitEasyMDE(container, data) {
const textarea = document.getElementById('feMdeTextarea');
_feState._originalContent = data.content;
const mde = new EasyMDE({
element: textarea,
const mde = createMDE(textarea, {
initialValue: data.content,
toolbar: [
'bold', 'italic', 'heading', '|',
'quote', 'unordered-list', 'ordered-list', '|',
'link', 'image', 'code', '|',
'preview', 'side-by-side', '|',
'guide'
],
status: false,
autofocus: false,
spellChecker: false,
autoDownloadFontAwesome: false,
previewClass: ['editor-preview', 'markdown'],
toolbar: ['bold', 'italic', 'heading', '|', 'quote', 'unordered-list', 'ordered-list', '|', 'link', 'image', 'code', '|', 'preview', 'side-by-side', '|', 'guide'],
minHeight: '100%',
maxHeight: '100%'
});
mde.codemirror.setOption('theme', 'default');
mde.codemirror.setOption('extraKeys', {
'Ctrl-S': () => feDoSave(),
'Cmd-S': () => feDoSave()
@@ -357,7 +344,6 @@ function feInitEasyMDE(container, data) {
feUpdateSelectionInfo(mde.codemirror, data);
});
// Änderungs-Tracking
mde.codemirror.on('change', () => {
const saveBtn = document.getElementById('feSaveBtn');
if (saveBtn) saveBtn.disabled = mde.value() === _feState._originalContent;
+3 -12
View File
@@ -205,12 +205,9 @@ registerPage('admin', {
});
// Speichern
saveBtn.addEventListener('click', async function() {
saveBtn.disabled = true;
saveBtn.addEventListener('click', () => withButtonLoading(saveBtn, saveBtn.textContent, async () => {
try {
if (editingUserId) {
// Update
var data = {
username: usernameInput.value.trim(),
displayName: displayNameInput.value.trim(),
@@ -219,15 +216,11 @@ registerPage('admin', {
};
await apiAdminUpdateUser(editingUserId, data);
// Passwort nur wenn ausgefüllt
var pw = passwordInput.value;
if (pw) {
await apiAdminResetPassword(editingUserId, pw);
}
if (pw) await apiAdminResetPassword(editingUserId, pw);
showToast('Benutzer aktualisiert');
} else {
// Create
var username = usernameInput.value.trim();
var password = passwordInput.value;
@@ -251,10 +244,8 @@ registerPage('admin', {
await loadUsers();
} catch (err) {
showToast(err.message, 'error');
} finally {
saveBtn.disabled = false;
}
});
}));
// ── Init: User laden ──
await loadUsers();
+24 -41
View File
@@ -27,26 +27,16 @@ registerPage('cronjob', {
for (const s of sessions) sessionMap[s.sessionId] = s;
// Session-Card rendern
function renderSessionSelectCard(cardEl, session, emptyHtml) {
cardEl.innerHTML = '';
if (!session) {
cardEl.innerHTML = emptyHtml;
return;
}
const agent = session.agentId ? agentMap[session.agentId] : null;
const inner = buildSessionCard(session, agent);
while (inner.firstChild) cardEl.appendChild(inner.firstChild);
cardEl.insertAdjacentHTML('beforeend', '<span class="session-select-change"><i class="fa fa-pencil"></i></span>');
function renderSessionSelectCard(cardEl, session, emptyIcon, emptyText) {
const inner = session ? buildSessionCard(session, session.agentId ? agentMap[session.agentId] : null) : null;
_renderCardPreview(cardEl, inner, emptyIcon, emptyText, 'session-select-change');
}
const sessionEmptyHtml = '<div class="session-select-empty"><i class="fa fa-comment-o"></i> Keine Session ausgewählt</div>';
const notifyEmptyHtml = '<div class="session-select-empty"><i class="fa fa-bell-o"></i> Keine (Antwort bleibt in Arbeits-Session)</div>';
// Session-Card click → Picker (nur Sessions mit Agent)
sessionCard.addEventListener('click', () => {
openSessionPickerModal(sessions, agentMap, sessionSelect.value, (selectedId) => {
sessionSelect.value = selectedId || '';
renderSessionSelectCard(sessionCard, selectedId ? sessionMap[selectedId] : null, sessionEmptyHtml);
renderSessionSelectCard(sessionCard, selectedId ? sessionMap[selectedId] : null, 'fa-comment-o', 'Keine Session ausgewählt');
}, { filterWithAgent: true, title: 'Session auswählen' });
});
@@ -54,7 +44,7 @@ registerPage('cronjob', {
notifyCard.addEventListener('click', () => {
openSessionPickerModal(sessions, agentMap, notifySelect.value, (selectedId) => {
notifySelect.value = selectedId || '';
renderSessionSelectCard(notifyCard, selectedId ? sessionMap[selectedId] : null, notifyEmptyHtml);
renderSessionSelectCard(notifyCard, selectedId ? sessionMap[selectedId] : null, 'fa-bell-o', 'Keine (Antwort bleibt in Arbeits-Session)');
}, { filterWithAgent: false, title: 'Benachrichtigungs-Session auswählen', noneLabel: 'Keine Benachrichtigung', noneIcon: 'fa-bell-slash' });
});
@@ -75,11 +65,11 @@ registerPage('cronjob', {
nameInput.value = job.name || '';
sessionSelect.value = job.sessionId || '';
renderSessionSelectCard(sessionCard, job.sessionId ? sessionMap[job.sessionId] : null, sessionEmptyHtml);
renderSessionSelectCard(sessionCard, job.sessionId ? sessionMap[job.sessionId] : null, 'fa-comment-o', 'Keine Session ausgewählt');
cronInput.value = job.cronExpression || '';
promptInput.value = job.prompt || '';
notifySelect.value = job.notifySessionId || '';
renderSessionSelectCard(notifyCard, job.notifySessionId ? sessionMap[job.notifySessionId] : null, notifyEmptyHtml);
renderSessionSelectCard(notifyCard, job.notifySessionId ? sessionMap[job.notifySessionId] : null, 'fa-bell-o', 'Keine (Antwort bleibt in Arbeits-Session)');
enabledCheckbox.checked = job.isEnabled;
updateCronPreview(cronInput, cronPreview);
@@ -94,9 +84,7 @@ registerPage('cronjob', {
});
// Jetzt ausführen
runBtn.addEventListener('click', async () => {
runBtn.disabled = true;
runBtn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Läuft...';
runBtn.addEventListener('click', () => withButtonLoading(runBtn, 'Läuft...', async () => {
try {
const result = await apiRunCronjob(params.id);
if (result.status === 'success') {
@@ -104,16 +92,12 @@ registerPage('cronjob', {
} else {
showToast('Fehler: ' + (result.error || 'Unbekannt'), 'error');
}
// Status neu laden
const updated = await apiGetCronjob(params.id);
if (updated) showCronjobStatus(statusBox, updated);
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
} finally {
runBtn.disabled = false;
runBtn.innerHTML = '<i class="fa fa-play"></i> Jetzt ausführen';
}
});
}));
}
// Speichern
@@ -132,23 +116,22 @@ registerPage('cronjob', {
return;
}
saveBtn.disabled = true;
try {
if (isEdit) {
await apiUpdateCronjob(params.id, data);
showToast('Cronjob gespeichert');
const updated = await apiGetCronjob(params.id);
if (updated) showCronjobStatus(statusBox, updated);
} else {
const created = await apiCreateCronjob(data);
showToast('Cronjob erstellt');
navigateTo('cronjob', { id: created.cronjobId });
await withButtonLoading(saveBtn, 'Speichern...', async () => {
try {
if (isEdit) {
await apiUpdateCronjob(params.id, data);
showToast('Cronjob gespeichert');
const updated = await apiGetCronjob(params.id);
if (updated) showCronjobStatus(statusBox, updated);
} else {
const created = await apiCreateCronjob(data);
showToast('Cronjob erstellt');
navigateTo('cronjob', { id: created.cronjobId });
}
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
}
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
} finally {
saveBtn.disabled = false;
}
});
});
}
});
+1 -7
View File
@@ -340,10 +340,4 @@ function _openCardMenu(trigger, dropdown) {
if (finalRect.left < 0) dropdown.style.left = '8px';
}
function formatDate(iso) {
if (!iso) return '';
return new Date(iso).toLocaleString('de-AT', {
day: '2-digit', month: '2-digit', year: '2-digit',
hour: '2-digit', minute: '2-digit'
});
}
+14 -31
View File
@@ -33,29 +33,18 @@ registerPage('profile', {
defaultPathInput.value = profile.defaultPath || '';
// Profil speichern
saveBtn.addEventListener('click', async function() {
saveBtn.disabled = true;
saveBtn.textContent = 'Speichern...';
saveBtn.addEventListener('click', () => withButtonLoading(saveBtn, 'Speichern...', async () => {
try {
var updated = await apiUpdateProfile({
displayName: displayNameInput.value.trim(),
persona: _profileMde.value().trim()
});
// currentUser aktualisieren
if (currentUser) {
currentUser.displayName = updated.displayName;
}
if (currentUser) currentUser.displayName = updated.displayName;
showToast('Profil gespeichert');
} catch (err) {
showToast(err.message, 'error');
} finally {
saveBtn.disabled = false;
saveBtn.textContent = 'Speichern';
}
});
}));
// Passwort ändern
passwordBtn.addEventListener('click', async function() {
@@ -73,23 +62,17 @@ registerPage('profile', {
return;
}
passwordBtn.disabled = true;
passwordBtn.textContent = 'Ändern...';
try {
await apiChangePassword(currentPw, newPw);
currentPwInput.value = '';
newPwInput.value = '';
confirmPwInput.value = '';
showToast('Passwort geändert');
} catch (err) {
showToast(err.message, 'error');
} finally {
passwordBtn.disabled = false;
passwordBtn.textContent = 'Passwort ändern';
}
await withButtonLoading(passwordBtn, 'Ändern...', async () => {
try {
await apiChangePassword(currentPw, newPw);
currentPwInput.value = '';
newPwInput.value = '';
confirmPwInput.value = '';
showToast('Passwort geändert');
} catch (err) {
showToast(err.message, 'error');
}
});
});
// Logout
+2 -6
View File
@@ -99,9 +99,7 @@ registerPage('server-settings', {
}
// Speichern
saveBtn.addEventListener('click', async function() {
saveBtn.disabled = true;
saveBtn.addEventListener('click', () => withButtonLoading(saveBtn, 'Speichern...', async () => {
try {
var data = {};
for (var j = 0; j < settings.length; j++) {
@@ -122,10 +120,8 @@ registerPage('server-settings', {
showToast(result.hint || 'Einstellungen gespeichert');
} catch (err) {
showToast(err.message, 'error');
} finally {
saveBtn.disabled = false;
}
});
}));
} catch (err) {
container.innerHTML = '<p class="form-error">Fehler beim Laden: ' + esc(err.message) + '</p>';
+27 -41
View File
@@ -85,15 +85,8 @@ registerPage('session', {
}
function renderAgentInCard(cardEl, agent, emptyIcon, emptyText) {
cardEl.innerHTML = '';
if (!agent) {
cardEl.innerHTML = '<div class="agent-select-empty"><i class="fa ' + emptyIcon + '"></i> ' + emptyText + '</div>';
return;
}
const skills = agentSkillsMap[agent.agentId] || [];
const inner = buildAgentCard(agent, skills);
while (inner.firstChild) cardEl.appendChild(inner.firstChild);
cardEl.insertAdjacentHTML('beforeend', '<span class="agent-select-change"><i class="fa fa-pencil"></i></span>');
const inner = agent ? buildAgentCard(agent, agentSkillsMap[agent.agentId] || []) : null;
_renderCardPreview(cardEl, inner, emptyIcon, emptyText, 'agent-select-change');
}
function updateReviewVisibility() {
@@ -264,45 +257,38 @@ function initSessionTelegram(sessionId) {
registerBtn.addEventListener('click', async function() {
var baseUrl = prompt('Base-URL der App:', window.location.origin);
if (!baseUrl) return;
registerBtn.disabled = true;
registerBtn.textContent = 'Wird registriert...';
try {
// Erst Token + Secret speichern (falls geändert)
await saveTelegramConfig(sessionId, tokenInput, secretInput);
var res = await apiSessionTelegramRegisterWebhook(sessionId, baseUrl.trim());
if (res.ok) {
showToast('Webhook registriert!', 'success');
} else {
showToast('Antwort: ' + JSON.stringify(res), 'error');
await withButtonLoading(registerBtn, 'Wird registriert...', async () => {
try {
await saveTelegramConfig(sessionId, tokenInput, secretInput);
var res = await apiSessionTelegramRegisterWebhook(sessionId, baseUrl.trim());
if (res.ok) {
showToast('Webhook registriert!', 'success');
} else {
showToast('Antwort: ' + JSON.stringify(res), 'error');
}
} catch(e) {
showToast('Fehler: ' + e.message, 'error');
}
} catch(e) {
showToast('Fehler: ' + e.message, 'error');
} finally {
registerBtn.disabled = false;
registerBtn.textContent = 'Webhook registrieren';
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
}
});
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
});
// Webhook entfernen
deleteBtn.addEventListener('click', async function() {
if (!confirm('Webhook wirklich entfernen?')) return;
deleteBtn.disabled = true;
deleteBtn.textContent = 'Wird entfernt...';
try {
var res = await apiSessionTelegramDeleteWebhook(sessionId);
if (res.ok) {
showToast('Webhook entfernt', 'success');
} else {
showToast('Antwort: ' + JSON.stringify(res), 'error');
await withButtonLoading(deleteBtn, 'Wird entfernt...', async () => {
try {
var res = await apiSessionTelegramDeleteWebhook(sessionId);
if (res.ok) {
showToast('Webhook entfernt', 'success');
} else {
showToast('Antwort: ' + JSON.stringify(res), 'error');
}
} catch(e) {
showToast('Fehler: ' + e.message, 'error');
}
} catch(e) {
showToast('Fehler: ' + e.message, 'error');
} finally {
deleteBtn.disabled = false;
deleteBtn.textContent = 'Webhook entfernen';
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
}
});
refreshTelegramStatus(sessionId, statusBox, registerBtn, deleteBtn);
});
}
+2 -7
View File
@@ -51,18 +51,13 @@ registerPage('settings', {
});
const backupBtn = document.getElementById('settingsBackupBtn');
if (backupBtn) backupBtn.addEventListener('click', async () => {
backupBtn.disabled = true;
backupBtn.textContent = 'Wird erstellt...';
if (backupBtn) backupBtn.addEventListener('click', () => withButtonLoading(backupBtn, 'Wird erstellt...', async () => {
try {
await apiBackupDb();
} catch (e) {
console.error('Backup fehlgeschlagen:', e);
} finally {
backupBtn.textContent = 'Jetzt sichern';
backupBtn.disabled = false;
}
});
}));
resetBtn.addEventListener('click', async () => {
if (!confirm('Alle Einstellungen auf Standard zurücksetzen?')) return;
+54
View File
@@ -81,6 +81,7 @@ function createMDE(element, options) {
autoDownloadFontAwesome: false,
autofocus: options.autofocus || false,
minHeight: options.minHeight || '120px',
maxHeight: options.maxHeight || undefined,
placeholder: options.placeholder || '',
previewClass: ['editor-preview', 'markdown']
});
@@ -89,6 +90,27 @@ function createMDE(element, options) {
return mde;
}
// ── Card Preview (for Picker-Select-Cards) ──
/**
* Renders a card preview into a select-card element.
* Used by agent picker (session-page) and session picker (cronjob-page).
* @param {HTMLElement} cardEl - The container element
* @param {HTMLElement|null} innerCard - Result of buildAgentCard/buildSessionCard, or null for empty state
* @param {string} emptyIcon - FA icon class for empty state
* @param {string} emptyText - Text for empty state
* @param {string} changeClass - CSS class for the change indicator (e.g. 'agent-select-change', 'session-select-change')
*/
function _renderCardPreview(cardEl, innerCard, emptyIcon, emptyText, changeClass) {
cardEl.innerHTML = '';
if (!innerCard) {
cardEl.innerHTML = '<div class="' + changeClass.replace('-change', '-empty') + '"><i class="fa ' + emptyIcon + '"></i> ' + emptyText + '</div>';
return;
}
while (innerCard.firstChild) cardEl.appendChild(innerCard.firstChild);
cardEl.insertAdjacentHTML('beforeend', '<span class="' + changeClass + '"><i class="fa fa-pencil"></i></span>');
}
// ── More/Expand Button ──
function initMoreButton(moreEl) {
@@ -100,6 +122,38 @@ function initMoreButton(moreEl) {
});
}
// ── Button Loading Helper ──
/**
* Disables a button, sets loading text, runs async function, then restores.
* @param {HTMLElement} btn - The button element
* @param {string} loadingText - Text while loading (e.g. 'Speichern...')
* @param {Function} asyncFn - Async function to run
* @returns {Promise<*>} Result of asyncFn
*/
async function withButtonLoading(btn, loadingText, asyncFn) {
const originalText = btn.textContent;
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.textContent = loadingText;
try {
return await asyncFn();
} finally {
btn.disabled = false;
btn.innerHTML = originalHtml;
}
}
// ── Date Formatting ──
function formatDate(iso) {
if (!iso) return '';
return new Date(iso).toLocaleString('de-AT', {
day: '2-digit', month: '2-digit', year: '2-digit',
hour: '2-digit', minute: '2-digit'
});
}
// ── Meta Bar ──
function setMeta(text) {