sync: mnogo
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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 (_) {
|
||||
|
||||
Reference in New Issue
Block a user