Bisher wurden Workflow-Subagent-Outputs nur beim sauberen FinalizeChain in die DB gerettet - bei Abbruch (529/Timeout/Stop/Hard-Kill) war alles weg, Tokens standen auf 0. - A) Inkrementelle Persistenz: MaybeFlushWorkflowProgress() (throttled 6s) schreibt workflow_info + Token + runId waehrend des Laufs -> ueberlebt Hard-Kill (OOM/Redeploy), wo kein catch/finally laeuft. - B) Output-Rettung: ReadLive liest jetzt vollstaendige Subagent-Outputs aus journal.jsonl (nicht nur Counts), inkl. State. - C) Token-Fix: neuer assistant-Branch -> CliEventType.Usage; EffectiveUsage() rettet Orchestrator-Token bei Abbruch statt 0. - D) Resume: Feld/Migration workflow_run_id; FinalizeChain persistiert die runId; naechster Chat injiziert einen resumeFromRunId-Hinweis. - UI: Workflow-Status-Modal zeigt geretteten Output + runId/"fortsetzbar"-Banner. Build 0/0, ReadLive gegen echtes journal.jsonl verifiziert, adversarial 2x geprueft. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
637 lines
23 KiB
JavaScript
637 lines
23 KiB
JavaScript
// ── Modal Helper ──
|
||
|
||
function _createModal(titleText, extraClasses) {
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'sp-overlay' + (extraClasses && extraClasses.overlay ? ' ' + extraClasses.overlay : '');
|
||
|
||
const modal = document.createElement('div');
|
||
modal.className = 'sp-modal' + (extraClasses && extraClasses.modal ? ' ' + extraClasses.modal : '');
|
||
|
||
const header = document.createElement('div');
|
||
header.className = 'sp-header';
|
||
const title = document.createElement('span');
|
||
title.textContent = titleText;
|
||
const closeBtn = document.createElement('button');
|
||
closeBtn.innerHTML = '<i class="fa fa-times"></i>';
|
||
closeBtn.className = 'sp-close icon-btn';
|
||
header.appendChild(title);
|
||
header.appendChild(closeBtn);
|
||
|
||
const body = document.createElement('div');
|
||
body.className = 'sp-body' + (extraClasses && extraClasses.body ? ' ' + extraClasses.body : '');
|
||
|
||
modal.appendChild(header);
|
||
modal.appendChild(body);
|
||
overlay.appendChild(modal);
|
||
document.body.appendChild(overlay);
|
||
|
||
const close = () => overlay.remove();
|
||
closeBtn.addEventListener('click', close);
|
||
overlay.addEventListener('click', (e) => {
|
||
if (e.target === overlay) close();
|
||
});
|
||
const onKey = (e) => {
|
||
if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); }
|
||
};
|
||
document.addEventListener('keydown', onKey);
|
||
|
||
return { overlay, modal, body, close };
|
||
}
|
||
|
||
// ── System-Prompt Modal ──
|
||
|
||
function _renderSystemPromptBlocks(container, blocks) {
|
||
for (var i = 0; i < blocks.length; i++) {
|
||
var block = blocks[i];
|
||
if (!block.content) continue;
|
||
|
||
var wrapper = document.createElement('div');
|
||
wrapper.className = 'sp-block';
|
||
|
||
var label = document.createElement('div');
|
||
label.className = 'sp-block-label';
|
||
label.innerHTML = '<i class="fa ' + esc(block.icon || 'fa-cog') + '"></i> ' + esc(block.label || 'Block');
|
||
wrapper.appendChild(label);
|
||
|
||
var content = document.createElement('div');
|
||
content.className = 'sp-block-content markdown';
|
||
content.innerHTML = renderMarkdown(block.content);
|
||
parseFluentEmojis(content);
|
||
wrapper.appendChild(content);
|
||
|
||
container.appendChild(wrapper);
|
||
}
|
||
}
|
||
|
||
async function showSystemPromptModal(sessionId) {
|
||
const { body } = _createModal('System-Prompt', { body: 'markdown' });
|
||
body.innerHTML = '<div class="sp-loading">Laden...</div>';
|
||
|
||
try {
|
||
const blocks = await apiGetSystemPromptBlocks(sessionId, typeof _planModeActive !== 'undefined' && _planModeActive);
|
||
body.innerHTML = '';
|
||
_renderSystemPromptBlocks(body, blocks);
|
||
} catch (e) {
|
||
body.innerHTML = '<div class="sp-loading text-danger">Fehler beim Laden des System-Prompts</div>';
|
||
}
|
||
}
|
||
|
||
// ── Payload Preview Modal ──
|
||
|
||
async function showPayloadPreviewModal(sessionId) {
|
||
const { body } = _createModal('Payload Preview', { modal: 'payload-preview-modal' });
|
||
body.innerHTML = '<div class="sp-loading">Laden...</div>';
|
||
|
||
try {
|
||
var planMode = typeof _planModeActive !== 'undefined' && _planModeActive;
|
||
var payload = await apiGetPayloadPreview(sessionId, planMode);
|
||
body.innerHTML = '';
|
||
|
||
var pre = document.createElement('pre');
|
||
pre.className = 'payload-json';
|
||
pre.textContent = JSON.stringify(payload, null, 2);
|
||
body.appendChild(pre);
|
||
} catch (e) {
|
||
body.innerHTML = '<div class="sp-loading text-danger">Fehler beim Laden des Payloads</div>';
|
||
}
|
||
}
|
||
|
||
// ── Workflow-Status Modal (Ultracode) ──
|
||
|
||
function _wfNum(n) { return Number(n || 0).toLocaleString('de-DE'); }
|
||
function _wfTime(t) { try { return new Date(t).toLocaleTimeString('de-DE'); } catch (e) { return ''; } }
|
||
function _wfDur(ms) {
|
||
ms = Number(ms || 0);
|
||
if (ms <= 0) return '–';
|
||
var s = Math.round(ms / 1000);
|
||
if (s < 60) return s + 's';
|
||
var m = Math.floor(s / 60);
|
||
return m + 'm ' + (s % 60) + 's';
|
||
}
|
||
|
||
function _renderWorkflowStatus(body, data) {
|
||
var status = (data && data.status) || 'none';
|
||
var wf = data && data.workflow;
|
||
|
||
var meta = {
|
||
running: { label: 'Workflow läuft', cls: 'wf-running', icon: 'fa-spinner fa-spin' },
|
||
completed: { label: 'Workflow abgeschlossen', cls: 'wf-done', icon: 'fa-check-circle' },
|
||
none: { label: 'Kein Workflow für diese Session', cls: 'wf-none', icon: 'fa-ban' }
|
||
};
|
||
var sm = meta[status] || meta.none;
|
||
|
||
body.innerHTML = '';
|
||
|
||
var head = document.createElement('div');
|
||
head.className = 'wf-status-head ' + sm.cls;
|
||
head.innerHTML = '<i class="fa ' + sm.icon + '"></i> ' + esc(sm.label);
|
||
body.appendChild(head);
|
||
|
||
if (!wf) {
|
||
var empty = document.createElement('div');
|
||
empty.className = 'wf-empty';
|
||
empty.textContent = 'Für diese Session wurde noch kein dynamischer Workflow (Ultracode) ausgeführt.';
|
||
body.appendChild(empty);
|
||
return;
|
||
}
|
||
|
||
// Name / Zusammenfassung des Workflows (nur nach Abschluss vorhanden).
|
||
if (wf.workflowName || wf.summary) {
|
||
var title = document.createElement('div');
|
||
title.className = 'wf-title';
|
||
title.innerHTML = (wf.workflowName ? '<span class="wf-name">' + esc(wf.workflowName) + '</span>' : '') +
|
||
(wf.summary ? '<span class="wf-summary">' + esc(wf.summary) + '</span>' : '');
|
||
body.appendChild(title);
|
||
}
|
||
|
||
// runId + Resume-Hinweis: Bei einem abgebrochenen Lauf kann der Agent den Workflow mit dieser
|
||
// runId (resumeFromRunId) fortsetzen – fertige Subagenten werden aus dem Journal wiederverwendet.
|
||
if (wf.runId) {
|
||
var rid = document.createElement('div');
|
||
rid.className = 'wf-meta';
|
||
rid.innerHTML = '<span title="Workflow-runId">runId: <code>' + esc(wf.runId) + '</code></span>' +
|
||
(status !== 'completed' ? ' <span class="wf-resume-hint">· fortsetzbar via resumeFromRunId</span>' : '');
|
||
body.appendChild(rid);
|
||
}
|
||
|
||
// Kennzahlen.
|
||
var stats = [
|
||
{ label: 'Subagenten', value: _wfNum(wf.subagentCount) },
|
||
{ label: 'Fertig', value: _wfNum(wf.completedCount) + ' / ' + _wfNum(wf.subagentCount) },
|
||
{ label: 'Tokens', value: _wfNum(wf.totalTokens) },
|
||
{ label: 'Tool-Aufrufe', value: _wfNum(wf.totalToolCalls) },
|
||
{ label: status === 'running' ? 'Läuft seit' : 'Dauer', value: _wfDur(wf.durationMs) }
|
||
];
|
||
var grid = document.createElement('div');
|
||
grid.className = 'wf-stat-grid';
|
||
stats.forEach(function (s) {
|
||
var cell = document.createElement('div');
|
||
cell.className = 'wf-stat';
|
||
cell.innerHTML = '<div class="wf-stat-val">' + esc(String(s.value)) +
|
||
'</div><div class="wf-stat-lbl">' + esc(s.label) + '</div>';
|
||
grid.appendChild(cell);
|
||
});
|
||
body.appendChild(grid);
|
||
|
||
// Phasen (nur nach Abschluss).
|
||
var phases = wf.phases || [];
|
||
if (phases.length) {
|
||
var psec = document.createElement('div');
|
||
psec.className = 'wf-section';
|
||
psec.innerHTML = '<div class="wf-section-lbl">Phasen</div>';
|
||
var plist = document.createElement('div');
|
||
plist.className = 'wf-phase-list';
|
||
phases.forEach(function (p) {
|
||
var row = document.createElement('div');
|
||
row.className = 'wf-phase-row';
|
||
row.innerHTML = '<span class="wf-phase-title">' + esc(p.title || '') + '</span>' +
|
||
(p.detail ? '<span class="wf-phase-detail">' + esc(p.detail) + '</span>' : '');
|
||
plist.appendChild(row);
|
||
});
|
||
psec.appendChild(plist);
|
||
body.appendChild(psec);
|
||
}
|
||
|
||
// Subagenten-Details. Nach Abschluss: Labels + Tokens/Tool-Calls/Dauer (aus <runId>.json).
|
||
// Live/bei Abbruch: agentId als Label + State + geretteter Output (aus journal.jsonl) – so ist
|
||
// die Arbeit fertiger Subagenten auch dann sichtbar, wenn der Lauf nie sauber endete.
|
||
var agents = wf.agents || [];
|
||
if (agents.length) {
|
||
var asec = document.createElement('div');
|
||
asec.className = 'wf-section';
|
||
asec.innerHTML = '<div class="wf-section-lbl">Subagenten</div>';
|
||
var alist = document.createElement('div');
|
||
alist.className = 'wf-agent-list';
|
||
agents.forEach(function (a) {
|
||
var st = a.state || '';
|
||
var stCls = st === 'done' ? 'wf-ag-done' : (st === 'running' ? 'wf-ag-running' : 'wf-ag-other');
|
||
var hasMetrics = (a.tokens || a.toolCalls || a.durationMs);
|
||
var row = document.createElement('div');
|
||
row.className = 'wf-agent-row';
|
||
row.innerHTML =
|
||
'<div class="wf-agent-main">' +
|
||
'<span class="wf-agent-state ' + stCls + '"></span>' +
|
||
'<span class="wf-agent-label">' + esc(a.label || ('Agent ' + (a.index || ''))) + '</span>' +
|
||
(a.phaseTitle ? '<span class="wf-agent-phase">' + esc(a.phaseTitle) + '</span>' : '') +
|
||
'</div>' +
|
||
(hasMetrics ?
|
||
'<div class="wf-agent-metrics">' +
|
||
'<span title="Tokens"><i class="fa fa-database"></i> ' + _wfNum(a.tokens) + '</span>' +
|
||
'<span title="Tool-Aufrufe"><i class="fa fa-wrench"></i> ' + _wfNum(a.toolCalls) + '</span>' +
|
||
'<span title="Dauer"><i class="fa fa-clock-o"></i> ' + _wfDur(a.durationMs) + '</span>' +
|
||
'</div>' : '') +
|
||
(a.resultPreview ? '<div class="wf-agent-output">' + esc(a.resultPreview) + '</div>' : '');
|
||
alist.appendChild(row);
|
||
});
|
||
asec.appendChild(alist);
|
||
body.appendChild(asec);
|
||
}
|
||
|
||
if (wf.startedAt) {
|
||
var ts = document.createElement('div');
|
||
ts.className = 'wf-meta';
|
||
ts.textContent = 'Start: ' + _wfTime(wf.startedAt);
|
||
body.appendChild(ts);
|
||
}
|
||
}
|
||
|
||
async function showWorkflowStatusModal(sessionId) {
|
||
const { body, overlay } = _createModal('Workflow-Status', { modal: 'workflow-status-modal' });
|
||
body.innerHTML = '<div class="sp-loading">Laden...</div>';
|
||
|
||
var poll = async function () {
|
||
try {
|
||
var data = await apiGetWorkflowStatus(sessionId);
|
||
_renderWorkflowStatus(body, data);
|
||
return data && data.status;
|
||
} catch (e) {
|
||
body.innerHTML = '<div class="sp-loading text-danger">Fehler beim Laden des Workflow-Status</div>';
|
||
return null;
|
||
}
|
||
};
|
||
|
||
var status = await poll();
|
||
|
||
// Live-Refresh, solange ein Workflow läuft – endet automatisch beim Schließen des Modals.
|
||
if (status === 'running') {
|
||
var timer = setInterval(async function () {
|
||
if (!document.body.contains(overlay)) { clearInterval(timer); return; }
|
||
var s = await poll();
|
||
if (s !== 'running') clearInterval(timer);
|
||
}, 2000);
|
||
}
|
||
}
|
||
|
||
// ── Command Output Modal ──
|
||
|
||
function showCommandOutputModal(name, command, result) {
|
||
const { body } = _createModal(name, { modal: 'cmd-output-modal' });
|
||
|
||
const exitCode = result.exitCode != null ? result.exitCode : '?';
|
||
const isError = exitCode !== 0;
|
||
const statusText = result.status === 'timeout' ? 'Timeout' : `Exit ${exitCode}`;
|
||
|
||
const meta = document.createElement('div');
|
||
meta.className = 'cmd-output-meta';
|
||
meta.innerHTML = `<code>${esc(command)}</code> <span class="cmd-exit-code${isError ? ' cmd-exit-error' : ''}">${esc(statusText)}</span>`;
|
||
body.appendChild(meta);
|
||
|
||
const output = document.createElement('pre');
|
||
output.className = 'cmd-output-content';
|
||
output.textContent = result.output || '(kein Output)';
|
||
body.appendChild(output);
|
||
}
|
||
|
||
// ── Krea Image Preview Modal ──
|
||
|
||
function showKreaImageModal(titleText, fullUrl) {
|
||
const { body } = _createModal(titleText, { overlay: 'krea-overlay', modal: 'krea-modal', body: 'krea-modal-body' });
|
||
|
||
var img = document.createElement('img');
|
||
img.src = fullUrl;
|
||
img.className = 'krea-modal-image';
|
||
img.alt = titleText;
|
||
img.addEventListener('error', function() {
|
||
body.innerHTML = '<div class="sp-loading text-danger">Bild konnte nicht geladen werden</div>';
|
||
});
|
||
|
||
body.appendChild(img);
|
||
}
|
||
|
||
// ── Picker Modal Helper ──
|
||
|
||
function _openPickerModal(title, items, currentId, onSelect, options) {
|
||
options = options || {};
|
||
const noneLabel = options.noneLabel || 'Keine Auswahl';
|
||
const noneIcon = options.noneIcon || 'fa-ban';
|
||
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'agent-picker-overlay';
|
||
|
||
const modal = document.createElement('div');
|
||
modal.className = 'agent-picker-modal';
|
||
|
||
const header = document.createElement('div');
|
||
header.className = 'agent-picker-header';
|
||
header.innerHTML = `<span>${esc(title)}</span><button class="icon-btn agent-picker-close"><i class="fa fa-times"></i></button>`;
|
||
modal.appendChild(header);
|
||
|
||
const body = document.createElement('div');
|
||
body.className = 'agent-picker-body';
|
||
|
||
// "Keine" Option
|
||
const noneCard = document.createElement('div');
|
||
noneCard.className = 'card agent-picker-item' + (!currentId ? ' selected' : '');
|
||
noneCard.innerHTML = `
|
||
<div class="card-image none-card-image"><i class="fa ${esc(noneIcon)} none-card-icon"></i></div>
|
||
<div class="card-body">
|
||
<div class="card-title-row">
|
||
<span class="card-title">${esc(noneLabel)}</span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
noneCard.addEventListener('click', () => {
|
||
onSelect(null);
|
||
overlay.remove();
|
||
});
|
||
body.appendChild(noneCard);
|
||
|
||
// Items
|
||
for (const item of items) {
|
||
const cardEl = options.buildCard(item);
|
||
cardEl.classList.add('agent-picker-item');
|
||
if (item._pickerId === currentId) cardEl.classList.add('selected');
|
||
cardEl.addEventListener('click', () => {
|
||
onSelect(item._pickerId);
|
||
overlay.remove();
|
||
});
|
||
body.appendChild(cardEl);
|
||
}
|
||
|
||
modal.appendChild(body);
|
||
overlay.appendChild(modal);
|
||
document.body.appendChild(overlay);
|
||
|
||
header.querySelector('.agent-picker-close').addEventListener('click', () => overlay.remove());
|
||
overlay.addEventListener('click', (e) => {
|
||
if (e.target === overlay) overlay.remove();
|
||
});
|
||
}
|
||
|
||
// ── Multi-Select Picker Modal ──
|
||
// Wie _openPickerModal, aber Mehrfachauswahl: Karten togglen ihren selected-Zustand,
|
||
// ein "Übernehmen"-Button bestätigt. onConfirm bekommt das Array der gewählten _pickerId.
|
||
|
||
function _openMultiPickerModal(title, items, selectedIds, onConfirm, options) {
|
||
options = options || {};
|
||
var selected = new Set(selectedIds || []);
|
||
|
||
var overlay = document.createElement('div');
|
||
overlay.className = 'agent-picker-overlay';
|
||
|
||
var modal = document.createElement('div');
|
||
modal.className = 'agent-picker-modal';
|
||
|
||
var header = document.createElement('div');
|
||
header.className = 'agent-picker-header';
|
||
header.innerHTML = '<span>' + esc(title) + '</span><button class="icon-btn agent-picker-close"><i class="fa fa-times"></i></button>';
|
||
modal.appendChild(header);
|
||
|
||
var body = document.createElement('div');
|
||
body.className = 'agent-picker-body';
|
||
|
||
for (var i = 0; i < items.length; i++) {
|
||
(function(item) {
|
||
var cardEl = options.buildCard(item);
|
||
cardEl.classList.add('agent-picker-item');
|
||
if (selected.has(item._pickerId)) cardEl.classList.add('selected');
|
||
cardEl.addEventListener('click', function() {
|
||
if (selected.has(item._pickerId)) { selected.delete(item._pickerId); cardEl.classList.remove('selected'); }
|
||
else { selected.add(item._pickerId); cardEl.classList.add('selected'); }
|
||
});
|
||
body.appendChild(cardEl);
|
||
})(items[i]);
|
||
}
|
||
modal.appendChild(body);
|
||
|
||
var footer = document.createElement('div');
|
||
footer.style.cssText = 'display:flex;justify-content:flex-end;gap:8px;padding:12px;border-top:1px solid var(--f2-border)';
|
||
var confirmBtn = document.createElement('button');
|
||
confirmBtn.className = 'add-cmd-btn';
|
||
confirmBtn.textContent = 'Übernehmen';
|
||
confirmBtn.addEventListener('click', function() {
|
||
onConfirm(Array.from(selected));
|
||
overlay.remove();
|
||
});
|
||
footer.appendChild(confirmBtn);
|
||
modal.appendChild(footer);
|
||
|
||
overlay.appendChild(modal);
|
||
document.body.appendChild(overlay);
|
||
|
||
header.querySelector('.agent-picker-close').addEventListener('click', function() { overlay.remove(); });
|
||
overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.remove(); });
|
||
}
|
||
|
||
// ── Telegram Bot Picker Modal ──
|
||
|
||
function openTelegramBotPickerModal(bots, currentBotId, onSelect) {
|
||
var items = bots.map(function(b) {
|
||
b._pickerId = b.botId;
|
||
return b;
|
||
});
|
||
_openPickerModal('Telegram Bot auswählen', items, currentBotId, onSelect, {
|
||
noneLabel: 'Kein Bot',
|
||
noneIcon: 'fa-paper-plane',
|
||
buildCard: function(b) { return buildTelegramBotCard(b); }
|
||
});
|
||
}
|
||
|
||
// ── OpenClaw Import Modal ──
|
||
|
||
async function showOpenClawImportModal(sessionId) {
|
||
const { body, close } = _createModal('OpenClaw Import', { modal: 'openclaw-import-modal' });
|
||
body.innerHTML = '<div class="sp-loading">Sessions werden geladen...</div>';
|
||
|
||
try {
|
||
const sessions = await apiGetOpenclawSessions();
|
||
|
||
if (!sessions || sessions.length === 0) {
|
||
body.innerHTML = '<div class="sp-loading">Keine OpenClaw-Sessions gefunden</div>';
|
||
return;
|
||
}
|
||
|
||
body.innerHTML = '';
|
||
|
||
var hint = document.createElement('div');
|
||
hint.className = 'openclaw-hint';
|
||
hint.textContent = 'Wähle eine OpenClaw-Session zum Importieren. Vorherige Imports werden aktualisiert.';
|
||
body.appendChild(hint);
|
||
|
||
var list = document.createElement('div');
|
||
list.className = 'openclaw-session-list';
|
||
|
||
for (var i = 0; i < sessions.length; i++) {
|
||
var s = sessions[i];
|
||
var card = document.createElement('div');
|
||
card.className = 'openclaw-session-card';
|
||
|
||
// Header
|
||
var header = document.createElement('div');
|
||
header.className = 'openclaw-card-header';
|
||
|
||
var idSpan = document.createElement('span');
|
||
idSpan.className = 'openclaw-session-id';
|
||
idSpan.textContent = s.sessionId.substring(0, 8) + '…';
|
||
idSpan.title = s.sessionId;
|
||
header.appendChild(idSpan);
|
||
|
||
var meta = document.createElement('span');
|
||
meta.className = 'openclaw-card-meta';
|
||
meta.innerHTML = '<span class="openclaw-meta-item"><i class="fa fa-calendar"></i> ' + esc(formatDate(s.createdAt)) + '</span>'
|
||
+ '<span class="openclaw-meta-item"><i class="fa fa-comments"></i> ' + s.totalMessages + ' Nachrichten</span>'
|
||
+ (s.model && s.model !== 'unknown' ? '<span class="openclaw-meta-item"><i class="fa fa-microchip"></i> ' + esc(s.model) + '</span>' : '');
|
||
header.appendChild(meta);
|
||
card.appendChild(header);
|
||
|
||
// Summary
|
||
if (s.summary) {
|
||
var summary = document.createElement('div');
|
||
summary.className = 'openclaw-summary';
|
||
// Take first line of summary that's meaningful (skip empty lines and headers)
|
||
var summaryLines = s.summary.split('\n').filter(function(l) { return l.trim() && !l.startsWith('#'); });
|
||
var summaryText = summaryLines.slice(0, 2).join(' ').trim();
|
||
if (summaryText.length > 150) summaryText = summaryText.substring(0, 150) + '…';
|
||
summary.textContent = summaryText || s.summary.substring(0, 150);
|
||
card.appendChild(summary);
|
||
}
|
||
|
||
// Preview messages
|
||
if (s.previewMessages && s.previewMessages.length > 0) {
|
||
var preview = document.createElement('div');
|
||
preview.className = 'openclaw-preview';
|
||
|
||
for (var j = 0; j < s.previewMessages.length; j++) {
|
||
var pm = s.previewMessages[j];
|
||
var line = document.createElement('div');
|
||
line.className = 'openclaw-preview-msg openclaw-preview-' + pm.role;
|
||
var icon = pm.role === 'user' ? 'fa-user' : 'fa-cog';
|
||
line.innerHTML = '<i class="fa ' + icon + '"></i> <span>' + esc(pm.text) + '</span>';
|
||
preview.appendChild(line);
|
||
}
|
||
card.appendChild(preview);
|
||
}
|
||
|
||
// Click handler - capture session in closure
|
||
(function(openclawSession) {
|
||
card.addEventListener('click', async function() {
|
||
// Confirm
|
||
var confirmed = confirm('OpenClaw-Session importieren?\n\nSession: ' + openclawSession.sessionId.substring(0, 8) + '…\n' + openclawSession.totalMessages + ' Nachrichten\n\nVorhandene Imports werden ersetzt.');
|
||
if (!confirmed) return;
|
||
|
||
card.classList.add('loading-state');
|
||
|
||
try {
|
||
var result = await apiImportOpenclawSession(openclawSession.sessionId, sessionId);
|
||
close();
|
||
showToast(result.messageCount + ' Nachrichten in ' + result.chainCount + ' Chains importiert', 'success');
|
||
} catch (e) {
|
||
card.classList.remove('loading-state');
|
||
showToast('Import fehlgeschlagen: ' + e.message, 'error');
|
||
}
|
||
});
|
||
})(s);
|
||
|
||
list.appendChild(card);
|
||
}
|
||
|
||
body.appendChild(list);
|
||
} catch (e) {
|
||
body.innerHTML = '<div class="sp-loading text-danger">Fehler: ' + esc(e.message) + '</div>';
|
||
}
|
||
}
|
||
|
||
// ── Remote-Verbindungen Modal ──
|
||
|
||
async function showRemoteModal(sessionId) {
|
||
const { body, close } = _createModal('Remote-Verbindungen');
|
||
|
||
body.innerHTML = '<div class="sp-loading">Laden...</div>';
|
||
|
||
async function renderConnections() {
|
||
try {
|
||
const connections = await apiGetRemoteConnections();
|
||
body.innerHTML = '';
|
||
|
||
var empty = document.createElement('div');
|
||
empty.className = 'sp-loading';
|
||
empty.innerHTML = '<div>Windows (PowerShell)</div>' +
|
||
'<code>irm ' + esc(location.origin) + '/remote.ps1 | iex</code>' +
|
||
'<div style="margin-top:8px">Linux (Bash)</div>' +
|
||
'<code>curl -s ' + esc(location.origin) + '/remote.sh | bash</code>';
|
||
body.appendChild(empty);
|
||
|
||
if (!connections || connections.length === 0) {
|
||
return;
|
||
}
|
||
|
||
for (var i = 0; i < connections.length; i++) {
|
||
var conn = connections[i];
|
||
var card = document.createElement('div');
|
||
card.className = 'sp-block';
|
||
card.classList.add('clickable');
|
||
|
||
var isAssigned = conn.assignedSessionId === sessionId;
|
||
var isAssignedOther = conn.assignedSessionId && conn.assignedSessionId !== sessionId;
|
||
|
||
var label = document.createElement('div');
|
||
label.className = 'sp-block-label';
|
||
var statusIcon = isAssigned ? 'fa-check-circle' : (isAssignedOther ? 'fa-link' : 'fa-desktop');
|
||
var statusClass = isAssigned ? ' text-primary' : '';
|
||
label.innerHTML = '<i class="fa ' + statusIcon + statusClass + '"></i> ' +
|
||
esc(conn.username || '?') + '@' + esc(conn.hostname || '?');
|
||
card.appendChild(label);
|
||
|
||
var content = document.createElement('div');
|
||
content.className = 'sp-block-content';
|
||
content.className = 'remote-card-content';
|
||
|
||
var info = document.createElement('span');
|
||
info.className = 'remote-card-info';
|
||
var connDate = new Date(conn.connectedAt);
|
||
info.textContent = conn.os + ' • Verbunden seit ' + connDate.toLocaleTimeString('de');
|
||
content.appendChild(info);
|
||
|
||
var btn = document.createElement('button');
|
||
if (isAssigned) {
|
||
btn.textContent = 'Trennen';
|
||
btn.classList.add('text-danger');
|
||
} else if (isAssignedOther) {
|
||
btn.textContent = 'Andere Session';
|
||
btn.disabled = true;
|
||
} else {
|
||
btn.textContent = 'Verbinden';
|
||
btn.classList.add('text-primary');
|
||
}
|
||
content.appendChild(btn);
|
||
|
||
card.appendChild(content);
|
||
body.appendChild(card);
|
||
|
||
// Event-Handler
|
||
(function(c, assigned) {
|
||
btn.addEventListener('click', async function(e) {
|
||
e.stopPropagation();
|
||
btn.disabled = true;
|
||
try {
|
||
if (assigned) {
|
||
await apiUnassignRemote(c.id);
|
||
} else {
|
||
await apiAssignRemote(c.id, sessionId);
|
||
}
|
||
await renderConnections();
|
||
} catch(err) {
|
||
showToast('Fehler: ' + err.message, 'error');
|
||
btn.disabled = false;
|
||
}
|
||
});
|
||
})(conn, isAssigned);
|
||
}
|
||
} catch (e) {
|
||
body.innerHTML = '<div class="sp-loading text-danger">Fehler beim Laden: ' + esc(e.message) + '</div>';
|
||
}
|
||
}
|
||
|
||
await renderConnections();
|
||
}
|
||
|
||
// Event-Delegation für Krea-Thumbnails im Chat
|
||
document.addEventListener('click', function(e) {
|
||
var ref = e.target.closest('.krea-ref');
|
||
if (ref) {
|
||
var fullUrl = ref.dataset.fullurl;
|
||
var filename = ref.dataset.filename;
|
||
if (fullUrl) showKreaImageModal(filename || 'Bild', fullUrl);
|
||
}
|
||
});
|