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:
1 parent
e4ccaf9dee
commit
e930268476
11 files changed
+137
-196
No files matched your search
+5
-30
@@ -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;
|
||||
|
||||
@@ -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, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>';
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user