sync: mnogo

This commit is contained in:
2025-10-03 21:55:24 +03:00
parent 2abfbb4b1a
commit 86182c0808
22 changed files with 4462 additions and 1469 deletions

View File

@@ -1463,4 +1463,142 @@ input[type="number"] {
}
/* --- Canvas preview sanitization (напоминание): хинты/лейблы/чекбоксы скрыты в превью --- */
/* Секции summary (headers/template) остаются видимыми */
/* Секции summary (headers/template) остаются видимыми */
/* --- Logs panel: base layout ------------------------------------------------- */
#logs-list { --log-border: #1f2b3b; }
#logs-list .logs-row {
padding: 8px 10px;
border-bottom: 1px solid var(--log-border);
background: #0f141a;
cursor: pointer;
transition: background-color .15s ease, box-shadow .15s ease, opacity .2s ease;
}
#logs-list .logs-row:hover { background: #111821; }
#logs-list .logs-row.selected {
outline: 0;
box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent-2) 40%, transparent);
}
#logs-list .logs-row.dim { opacity: .70; }
#logs-list .logs-row .title { font-size: 13px; }
#logs-list .logs-row .sub {
font-size: 11px;
opacity: .85;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* --- HTTP status styling ----------------------------------------------------- */
/* Shimmer animation for pending HTTP rows */
@keyframes logs-shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* Pending request (no response yet): blue accent shimmer using --accent-2 */
#logs-list .logs-row.kind-http.http-pending {
border-left: 3px solid var(--accent-2);
background:
linear-gradient(90deg,
color-mix(in srgb, var(--accent-2) 10%, transparent) 0%,
color-mix(in srgb, var(--accent-2) 20%, transparent) 50%,
color-mix(in srgb, var(--accent-2) 10%, transparent) 100%);
background-size: 200% 100%;
animation: logs-shimmer 1.4s linear infinite;
}
/* Success/Failure borders for completed HTTP rows */
#logs-list .logs-row.kind-http.http-ok { border-left: 3px solid #10b981; } /* emerald-500 */
#logs-list .logs-row.kind-http.http-err { border-left: 3px solid #ef4444; } /* red-500 */
/* --- Node sleep pulse -------------------------------------------------------- */
@keyframes logs-sleep-pulse {
0% { box-shadow: inset 0 0 0 0 rgba(245,158,11, 0.00); }
50% { box-shadow: inset 0 0 0 2px rgba(245,158,11, 0.35); }
100% { box-shadow: inset 0 0 0 0 rgba(245,158,11, 0.00); }
}
#logs-list .logs-row.kind-node.ev-sleep {
border-left: 3px dashed #f59e0b; /* amber-500 */
animation: logs-sleep-pulse 1.8s ease-in-out infinite;
}
/* --- Node "water" running effect (blue→violet gradient, slow, saturated) ----- */
@keyframes node-water {
0% { background-position: 0% 0%; }
50% { background-position: 100% 100%; }
100% { background-position: 0% 0%; }
}
/* Применяется, когда нода исполняется (класс .node-running вешается из SSE) */
#drawflow .drawflow-node.node-running .title-box,
#drawflow .drawflow-node.node-running .box {
/* База: наш стандартный фон ноды, сверху — насыщенный градиент воды */
background:
linear-gradient(125deg,
color-mix(in srgb, #60a5fa 78%, transparent) 0%,
color-mix(in srgb, #7c3aed 56%, transparent) 50%,
color-mix(in srgb, #60a5fa 78%, transparent) 100%),
var(--node);
background-size: 360% 360%;
animation: node-water 5.0s ease-in-out infinite; /* медленнее и гуще, «водная гладь» */
border-color: color-mix(in srgb, var(--accent-2) 55%, #7c3aed 45%);
/* Лёгкое свечение, чтобы подчеркнуть активность, без ядовитости */
box-shadow:
0 0 0 2px color-mix(in srgb, var(--accent-2) 30%, transparent),
0 0 16px rgba(96,165,250,.18),
inset 0 0 22px rgba(167,139,250,.12);
}
/* Безопасность: при успехе/ошибке временные классы могут перебить рамку */
#drawflow .drawflow-node.node-ok .title-box,
#drawflow .drawflow-node.node-ok .box {
border-color: #10b981 !important; /* emerald */
box-shadow:
0 0 0 2px color-mix(in srgb, #10b981 35%, transparent),
0 0 12px rgba(16,185,129,.18);
background-image: none; /* убрать «воду» после окончания */
}
#drawflow .drawflow-node.node-err .title-box,
#drawflow .drawflow-node.node-err .box {
border-color: #ef4444 !important; /* red */
box-shadow:
0 0 0 2px color-mix(in srgb, #ef4444 35%, transparent),
0 0 12px rgba(239,68,68,.18);
background-image: none; /* убрать «воду» после ошибки */
}
/* --- Water overlay: full-node coverage with fade-out on stop ------------------ */
/* База: прозрачный градиент-оверлей на ВСЕЙ .drawflow_content_node,
который плавно меняет прозрачность. Когда нода активна (.node-running) —
поднимаем непрозрачность и двигаем «волну». При снятии .node-running
оверлей сам «затухает» благодаря transition на opacity. */
#drawflow .drawflow-node .drawflow_content_node {
position: relative;
overflow: hidden;
z-index: 0; /* чтобы ::before можно было поднять поверх */
}
#drawflow .drawflow-node .drawflow_content_node::before {
content: '';
position: absolute;
inset: 0;
z-index: 1; /* поверх содержимого ноды, но без кликов */
pointer-events: none;
border-radius: 10px;
background: linear-gradient(125deg,
color-mix(in srgb, #60a5fa 78%, transparent) 0%,
color-mix(in srgb, #7c3aed 56%, transparent) 50%,
color-mix(in srgb, #60a5fa 78%, transparent) 100%);
background-size: 360% 360%;
opacity: 0; /* по умолчанию невидим */
transition: opacity 1.4s ease-in-out; /* «затухание» при остановке */
}
#drawflow .drawflow-node.node-running .drawflow_content_node::before {
opacity: .42; /* насыщенно, но читаемо; плавно исчезает при снятии класса */
animation: node-water 5.0s ease-in-out infinite; /* медленная водная гладь */
}
/* Отключаем прежнюю «водную» анимацию на частях, оставляя оверлей на всю ноду */
#drawflow .drawflow-node.node-running .title-box,
#drawflow .drawflow-node.node-running .box {
background: var(--node) !important;
animation: none !important;
}

View File

@@ -32,6 +32,34 @@
header .actions { display: flex; gap: 8px; }
button { background: #1f2937; border: 1px solid #334155; color: #e5e7eb; padding: 6px 10px; border-radius: 8px; cursor: pointer; }
button:hover { background: #273246; }
/* Split STOP button styling */
.chip-btn.split {
position: relative;
display: inline-flex;
padding: 0;
overflow: hidden;
border-radius: 8px;
}
.chip-btn.split .seg {
padding: 6px 10px;
display: inline-block;
line-height: 1.2;
user-select: none;
}
.chip-btn.split .seg-left {
border-right: 1px solid rgba(255,255,255,0.08);
}
.chip-btn.split.hover-left .seg-left {
background: color-mix(in srgb, #f59e0b 28%, transparent);
}
.chip-btn.split.hover-right .seg-right {
background: color-mix(in srgb, #ef4444 28%, transparent);
}
.chip-btn.split.is-busy {
opacity: .7;
pointer-events: none;
}
#container { display: grid; grid-template-columns: 260px 1fr 360px; height: calc(100vh - 52px); }
#sidebar { border-right: 1px solid var(--border); padding: 12px; background: var(--panel); overflow: auto; }
#canvas { position: relative; }
@@ -93,7 +121,11 @@
<button class="chip-btn" id="btn-scheme" title="Показать мини‑схему">СХЕМА</button>
<button class="chip-btn" id="btn-tidy" title="Авто‑раскладка графа (повторный клик — отмена)">РАСКЛАДКА</button>
<button class="chip-btn" id="btn-logs" title="Журнал HTTP запросов/ответов">ЛОГИ</button>
<button class="chip-btn" id="btn-cancel" title="Прервать текущее исполнение пайплайна">СТОП ⏹</button>
<!-- Split STOP button: left=graceful, right=abort -->
<button class="chip-btn split" id="btn-cancel" title="СТОП: левая половина — мягкая (ждать), правая — жёсткая (обрыв)">
<span class="seg seg-left" aria-label="мягкая">СТ</span>
<span class="seg seg-right" aria-label="жёсткая">ОП</span>
</button>
<a class="chip-btn" href="/" role="button">ДОМОЙ</a>
</div>
<!-- Danmaku overlay layer -->
@@ -178,8 +210,14 @@
<div id="logs-list" style="border:1px solid #2b3646;border-radius:8px;overflow:auto;background:#0f141a"></div>
<div id="logs-detail" style="display:flex;flex-direction:column;gap:8px">
<div>
<strong>Request</strong>
<pre id="logs-req" style="min-height:120px;max-height:36vh;overflow:auto;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere"></pre>
<div style="display:flex;align-items:center;gap:8px;justify-content:space-between">
<strong>Request</strong>
<div class="logs-req-actions" style="display:flex;gap:8px">
<button id="logs-send" title="Отправить отредактированный запрос">Отправить</button>
<button id="logs-revert" title="Вернуть оригинальный запрос">Вернуть</button>
</div>
</div>
<pre id="logs-req" contenteditable="true" tabindex="0" style="min-height:120px;max-height:36vh;overflow:auto;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere"></pre>
</div>
<div>
<strong>Response</strong>
@@ -293,7 +331,7 @@
const NODE_IO = {
// depends: используется только для порядка выполнения (зависимости), данные не читаются
// Провода не переносят данные; OUT/vars берутся из контекста и снапшота.
SetVars: { inputs: [], outputs: ['done'] },
SetVars: { inputs: ['depends'], outputs: ['done'] },
If: { inputs: ['depends'], outputs: ['true','false'] },
ProviderCall:{ inputs: ['depends'], outputs: ['done'] },
RawForward: { inputs: ['depends'], outputs: ['done'] },
@@ -1293,6 +1331,7 @@
<label>headers (JSON)</label><textarea id="f-headers" rows="4">${escText(cfg.headers||'{}')}</textarea>
<label>template (JSON)</label>
<textarea id="f-template" rows="10">${escText(cfg.template||'{}')}</textarea>
<label id="row-claude-no-system" style="display:${((data.provider||'openai').toLowerCase()==='claude') ? 'inline-flex' : 'none'}; align-items:center; gap:6px"><input id="f-claude-no-system" type="checkbox" ${(data.claude_no_system===true)?'checked':''}> claude_no_system</label>
<div style="margin-top:6px">
<details class="help">
<summary title="Подсказка по шаблону">?</summary>
@@ -1312,6 +1351,15 @@
<div class="hint">
Пример: <code>[[VAR:incoming.json.contents]] & [[PROMPT]]</code>. Пусто — выключено. Итог автоматически приводится к структуре выбранного провайдера (messages/contents/system).
</div>
<label>prompt_preprocess (pre-merge DSL)</label>
<textarea id="f-prompt-preprocess" rows="3">${escText(data.prompt_preprocess || '')}</textarea>
<div class="hint">
Каждая строка: <code>SEGMENT [delKeyContains "строка"] [delpos=prepend|append|N|-1] [case=ci|cs] [pruneEmpty]</code>.
По умолчанию: case=ci, pruneEmpty=false, без delpos → append. Примеры:
<br/><code>[[VAR:incoming.json.contents]] delKeyContains "Текст" delpos=-1</code>
<br/><code>[[VAR:incoming.json.messages]] delKeyContains "debug" case=cs</code>
<br/>SEGMENT поддерживает макросы [[...]] и {{ ... }}. Выполняется ДО prompt_combine.
</div>
`;
html += `
<div class="group-title" style="margin-top:16px">Prompt Blocks</div>
@@ -1465,6 +1513,7 @@
if (inp.id === 'f-baseurl') cfg.base_url = inp.value;
if (inp.id === 'f-endpoint') cfg.endpoint = inp.value;
if (inp.id === 'f-headers') cfg.headers = inp.value;
if (inp.id === 'f-claude-no-system') d.claude_no_system = !!inp.checked;
if (inp.id === 'f-provider') d.provider = inp.value; // select changes provider
if (inp.id === 'f-prompt-combine') {
const val = String(inp.value || '').trim();
@@ -1474,6 +1523,14 @@
delete d.prompt_combine;
}
}
if (inp.id === 'f-prompt-preprocess') {
const val = String(inp.value || '').trim();
if (val) {
d.prompt_preprocess = inp.value;
} else {
delete d.prompt_preprocess;
}
}
// Sleep controls (seconds + enable checkbox)
if (inp.id === 'f-sleep-en') {
const secEl = document.getElementById('f-sleep-sec');
@@ -1556,6 +1613,7 @@
if (inp.id === 'f-model') d.model = inp.value;
if (inp.id === 'f-extra') d.extra_headers = inp.value;
if (inp.id === 'f-override') d.override_path = inp.value;
if (inp.id === 'f-baseurl') d.base_url = inp.value;
if (inp.id === 'f-pass') d.passthrough_headers = inp.checked;
// Sleep controls (seconds + enable checkbox)
if (inp.id === 'f-sleep-en') {
@@ -1802,6 +1860,10 @@
if (el) el.__data = d;
}
} catch (e) {}
try {
const rowCns = document.getElementById('row-claude-no-system');
if (rowCns) rowCns.style.display = (d.provider === 'claude' ? 'inline-flex' : 'none');
} catch (_){}
try { console.debug('[ProviderCall] provider switched to', d.provider, cfg); } catch (e) {}
});
}
@@ -1867,6 +1929,71 @@
const elN = document.querySelector(`#node-${id}`);
if (elN) elN.__data = JSON.parse(JSON.stringify(ncheck.data || {}));
}
// JSON5 validation for headers/extra_headers (normalize to strict JSON)
try {
function __attachJsonValidation(el, opts) {
if (!el) return;
const wantObject = !!(opts && opts.wantObject);
const normalize = !!(opts && opts.normalize !== false);
const good = () => { try { el.style.borderColor=''; el.title=''; } catch(_){} };
const bad = (msg) => { try { el.style.borderColor = '#e11d48'; el.title = msg || 'Invalid JSON'; } catch(_){} };
const parseAndMark = () => {
try {
const txt = String(el.value || '').trim();
if (!txt) { good(); return; }
let obj = JSON5.parse(txt);
if (wantObject && (typeof obj !== 'object' || obj === null || Array.isArray(obj))) { bad('Ожидается JSON-объект { ... }'); return; }
if (normalize) {
try { el.value = JSON.stringify(obj, null, 2); } catch(_){}
}
good();
} catch (e) {
bad('Ошибка JSON: ' + (e && e.message ? e.message : 'parse error'));
}
};
el.addEventListener('blur', parseAndMark);
el.addEventListener('input', () => { try { el.style.borderColor=''; el.title=''; } catch(_){} });
}
if (type === 'ProviderCall') {
__attachJsonValidation(document.getElementById('f-headers'), { wantObject: true, normalize: true });
} else if (type === 'RawForward') {
__attachJsonValidation(document.getElementById('f-extra'), { wantObject: true, normalize: true });
}
} catch (_) {}
// JSON5 validation for template (macro-aware)
try {
(function(){
const tplEl = document.getElementById('f-template');
if (tplEl) {
const good = () => { try { tplEl.style.borderColor=''; tplEl.title=''; } catch(_){} };
const bad = (msg) => { try { tplEl.style.borderColor='#e11d48'; tplEl.title=(msg||'Invalid JSON template'); } catch(_){} };
const parseAndMark = () => {
try {
let txt = String(tplEl.value || '').trim();
if (!txt) { good(); return; }
let s = txt;
// Neutralize templating macros so JSON5.parse won't choke:
// 1) Replace any {{ ... }} with a scalar value
s = s.replace(/{{[\s\S]*?}}/g, '0');
// 2) Replace [[PROMPT]] with a dummy property to keep object shape valid
s = s.replace(/\[\[\s*PROMPT\s*\]\]/g, '"__PROMPT__":true');
// Tolerant parse (JSON5 supports unquoted keys, trailing commas, etc.)
const obj = JSON5.parse(s);
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
bad('Шаблон должен быть JSON-объектом');
return;
}
good();
} catch (e) {
bad('Ошибка JSON шаблона: ' + (e && e.message ? e.message : 'parse error'));
}
};
tplEl.addEventListener('blur', parseAndMark);
tplEl.addEventListener('input', () => { try { tplEl.style.borderColor=''; tplEl.title=''; } catch(_){} });
}
})();
} catch (_) {}
// Prompt Manager UI for ProviderCall
if (type === 'ProviderCall') { PM.setupProviderCallPMUI(editor, id); }
}
@@ -2536,26 +2663,79 @@
loadPipeline();
refreshPresets();
// Wire manual cancel button
// Wire split STOP button (left=graceful, right=abort)
try {
const btnCancel = document.getElementById('btn-cancel');
if (btnCancel) {
btnCancel.addEventListener('click', async () => {
const btnSplit = document.getElementById('btn-cancel');
if (btnSplit) {
const leftSeg = btnSplit.querySelector('.seg-left');
const rightSeg = btnSplit.querySelector('.seg-right');
function sideFromEvent(ev) {
const r = btnSplit.getBoundingClientRect();
const x = (ev.touches && ev.touches[0] ? ev.touches[0].clientX : ev.clientX) - r.left;
return (x < r.width / 2) ? 'left' : 'right';
}
function setHover(side) {
btnSplit.classList.toggle('hover-left', side === 'left');
btnSplit.classList.toggle('hover-right', side === 'right');
}
async function postCancel(url, side) {
try {
btnCancel.disabled = true;
btnCancel.textContent = 'СТОП…';
const res = await fetch('/admin/cancel', { method: 'POST' });
btnSplit.classList.add('is-busy');
if (side === 'left' && leftSeg) leftSeg.textContent = 'СТ…';
if (side === 'right' && rightSeg) rightSeg.textContent = 'ОП…';
const res = await fetch(url, { method: 'POST' });
if (res.ok) {
status('Отмена исполнения запрошена');
const mode = url.includes('/abort') ? 'обрыв' : 'мягкая отмена';
status('Отмена исполнения: ' + mode + ' запрошена');
} else {
status('Ошибка запроса отмены: ' + res.status);
}
} catch (e) {
status('Ошибка запроса отмены');
} finally {
setTimeout(()=>{ try { btnCancel.disabled = false; btnCancel.textContent = 'СТОП ⏹'; } catch(_){} }, 600);
setTimeout(()=>{
try {
btnSplit.classList.remove('is-busy');
if (leftSeg) leftSeg.textContent = 'СТ ⏹';
if (rightSeg) rightSeg.textContent = 'ОП';
} catch(_){}
}, 600);
}
}
btnSplit.addEventListener('mousemove', (ev) => {
setHover(sideFromEvent(ev));
}, { passive: true });
btnSplit.addEventListener('mouseleave', () => {
btnSplit.classList.remove('hover-left','hover-right');
}, { passive: true });
btnSplit.addEventListener('click', async (ev) => {
const side = sideFromEvent(ev);
if (side === 'left') {
await postCancel('/admin/cancel', 'left');
} else {
await postCancel('/admin/cancel/abort', 'right');
}
});
// Touch support
btnSplit.addEventListener('touchstart', (ev) => {
setHover(sideFromEvent(ev));
}, { passive: true });
btnSplit.addEventListener('touchend', async (ev) => {
const side = sideFromEvent(ev);
btnSplit.classList.remove('hover-left','hover-right');
if (side === 'left') {
await postCancel('/admin/cancel', 'left');
} else {
await postCancel('/admin/cancel/abort', 'right');
}
}, { passive: false });
}
} catch(_) {}
@@ -3557,6 +3737,59 @@ const __busyFav = (function(){
const dataPre = document.getElementById('logs-data');
const logsDetail = document.getElementById('logs-detail');
// Manual resend editor state (edited/original per-log)
const btnReqSend = document.getElementById('logs-send');
const btnReqRevert = document.getElementById('logs-revert');
const __reqOriginalById = new Map();
const __reqEditedById = new Map();
function getSelectedLog() {
try { return selectedLogId ? logsById.get(selectedLogId) : null; } catch(_) { return null; }
}
function isHttpLog(it) {
try { return !!(it && (it.kind === 'http' || it.req)); } catch(_) { return false; }
}
function updateReqButtons() {
try {
const it = getSelectedLog();
const en = isHttpLog(it);
if (btnReqSend) btnReqSend.disabled = !en;
if (btnReqRevert) btnReqRevert.disabled = !en;
} catch(_) {}
}
if (reqPre) {
try { reqPre.setAttribute('contenteditable','true'); } catch(_) {}
reqPre.addEventListener('input', () => {
try { if (selectedLogId) __reqEditedById.set(selectedLogId, reqPre.innerText); } catch(_){}
});
}
if (btnReqSend) btnReqSend.addEventListener('click', async () => {
const it = getSelectedLog();
if (!isHttpLog(it)) return;
const reqText = (reqPre && reqPre.innerText!=null) ? reqPre.innerText : '';
const body = { req_id: it.id, request_text: reqText, prefer_registry_original: true };
try {
const res = await fetch('/admin/http/manual-send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
let j = null; try { j = await res.json(); } catch(_){}
try { status('Manual send: ' + (res.ok ? 'ok' : ('error ' + res.status)) + (j && j.req_id ? (' • new req=' + j.req_id) : '')); } catch(_){}
} catch (e) {
try { status('Manual send error'); } catch(_){}
}
});
if (btnReqRevert) btnReqRevert.addEventListener('click', () => {
const it = getSelectedLog();
if (!isHttpLog(it)) return;
try {
const orig = __reqOriginalById.get(it.id) || (it && it.req ? buildReqText(it.req) : '');
__reqEditedById.delete(it.id);
if (reqPre) reqPre.textContent = orig || '';
} catch(_) {}
});
// Простая изоляция выделения для Request/Response/Data: без pointer-events, без «замков»
(function simpleLogsSelectionIsolation(){
const detail = logsDetail || document.getElementById('logs-detail');
@@ -3623,7 +3856,7 @@ const __busyFav = (function(){
const btnLogsClose = document.getElementById('logs-close');
const btnLogsClear = document.getElementById('logs-clear');
function isLogsOpen(){ return panelLogs && panelLogs.style.display !== 'none'; }
function openLogs(){ if (panelLogs) { panelLogs.style.display='block'; panelLogs.setAttribute('aria-hidden','false'); try { if (logsDetail) logsDetail.setAttribute('data-active-pre','logs-req'); } catch(_){} renderLogsList(); renderLogsDetail(selectedLogId); } }
function openLogs(){ if (panelLogs) { panelLogs.style.display='block'; panelLogs.setAttribute('aria-hidden','false'); try { if (logsDetail) logsDetail.setAttribute('data-active-pre','logs-req'); } catch(_){} renderLogsList(); renderLogsDetail(selectedLogId); try { updateReqButtons && updateReqButtons(); } catch(_){} } }
function closeLogs(){ if (panelLogs) { panelLogs.style.display='none'; panelLogs.setAttribute('aria-hidden','true'); } }
if (btnLogsOpen) btnLogsOpen.addEventListener('click', () => { if (isLogsOpen()) { closeLogs(); } else { openLogs(); } });
if (btnLogsClose) btnLogsClose.addEventListener('click', closeLogs);
@@ -3710,10 +3943,15 @@ const __busyFav = (function(){
}
if (item.kind === 'http') {
st = (item.res && item.res.status!=null) ? `${item.res.status}` : '';
const hasResp = !!(item.res && item.res.status != null);
const stc = hasResp ? Number(item.res.status) : 0;
const ok = hasResp && stc >= 200 && stc < 400;
// Показать индикатор ожидания для активного HTTP
st = hasResp ? `${stc}` : ' • …';
classes.push('kind-http');
const stc = (item.res && typeof item.res.status === 'number') ? item.res.status : 0;
if (stc >= 200 && stc < 400) classes.push('http-ok'); else if (stc) classes.push('http-err');
if (!hasResp) classes.push('http-pending');
else if (ok) classes.push('http-ok');
else classes.push('http-err');
} else if (item.kind === 'node') {
const ev = (item.ev ? String(item.ev) : '').toLowerCase();
const dur = (item.duration_ms!=null) ? ` (${fmtMs(item.duration_ms)})` : '';
@@ -3787,7 +4025,15 @@ const __busyFav = (function(){
if (it.kind === 'http' || (!it.kind && it.req)) {
// HTTP logs: show full structure, trim only base64 values; render images in Data
reqPre.textContent = buildReqText(it && it.req);
try {
const __origTxt = buildReqText(it && it.req);
try { __reqOriginalById.set(it.id, __origTxt); } catch(_){}
const __editedTxt = (typeof __reqEditedById !== 'undefined' && __reqEditedById && __reqEditedById.get) ? __reqEditedById.get(it.id) : null;
reqPre.textContent = (typeof __editedTxt === 'string') ? __editedTxt : __origTxt;
} catch(_) {
reqPre.textContent = buildReqText(it && it.req);
}
try { updateReqButtons && updateReqButtons(); } catch(_){}
if (it && it.res) {
const raw = it.res.body_text || '';
let shown = raw;
@@ -4153,9 +4399,19 @@ const __busyFav = (function(){
const data = JSON.parse(e.data);
// Special handling for manual cancel notification
if (data && data.event === 'cancelled') {
try { status('Исполнение остановлено пользователем'); } catch(_){}
try { __busyFav.reset(); } catch(_){}
}
try { status('Исполнение остановлено пользователем'); } catch(_) {}
try { __busyFav.reset(); } catch(_) {}
// Завершаем все висящие HTTP-записи (убираем анимацию ожидания)
try {
const now = Date.now();
for (const it of logs) {
if (it && it.kind === 'http' && !it.res) {
it.res = { status: 0, headers: {}, body_text: 'Cancelled by user (abort)', ts: now, data_preview: 'cancelled' };
}
}
if (isLogsOpen()) { renderLogsList(); if (selectedLogId) renderLogsDetail(selectedLogId); }
} catch (_) {}
}
handleTraceEvent(data);
handleLogEvent(data);
} catch (_) {