112 lines
5.1 KiB
JavaScript
112 lines
5.1 KiB
JavaScript
// utils.js — общие хелперы фронтенда
|
||
// Используется ВСЕМИ модулями (должен грузиться ПЕРВЫМ).
|
||
// Экспортирует (в глобальную область): _esc, validateJson, relativeTime,
|
||
// startLogPoll, toggleLog
|
||
|
||
/**
|
||
* HTML-экранирование — защита от XSS.
|
||
* Все данные из API/БД пропускаются через _esc() перед вставкой в innerHTML.
|
||
*
|
||
* @param {*} s — что угодно (приводится к строке через String())
|
||
* @returns {string} безопасная для HTML строка
|
||
*
|
||
* Заменяет:
|
||
* & → & (первым! иначе сломает уже заменённые сущности)
|
||
* " → " (атрибуты в кавычках)
|
||
* < → < (XSS-вектор: <script>)
|
||
*
|
||
* > и ' НЕ экранируем — в нашем контексте они безопасны,
|
||
* а лишние замены портят читаемость.
|
||
*/
|
||
function _esc(s){
|
||
return String(s||'').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<');
|
||
}
|
||
|
||
/**
|
||
* Проверить что значение в поле — валидный JSON.
|
||
* Используется для map-параметров перед отправкой формы.
|
||
*
|
||
* @param {HTMLElement} el — input/textarea элемент
|
||
* @param {boolean} quiet — true = не менять внешний вид (batch-проверка)
|
||
* @returns {boolean} true если JSON валидный или поле пустое
|
||
*/
|
||
function validateJson(el,quiet){
|
||
// Ищем соседний span.json-err для показа ошибки
|
||
const errEl=el.parentElement.querySelector('.json-err');
|
||
const v=el.value.trim();
|
||
// Пустое поле — ок (не ошибка)
|
||
if(!v) return true;
|
||
try{ JSON.parse(v); }
|
||
catch(e){
|
||
// Показываем ошибку в span.json-err (если не quiet)
|
||
if(!quiet&&errEl){ errEl.style.display='inline'; errEl.textContent='Ошибка JSON: '+e.message; }
|
||
el.style.borderColor='var(--destructive)';
|
||
return false;
|
||
}
|
||
// Всё ок — убираем ошибку
|
||
if(!quiet&&errEl) errEl.style.display='none';
|
||
el.style.borderColor='';
|
||
return true;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// Панель логов — поллинг /api/log каждые 2 секунды
|
||
// ═══════════════════════════════════════════════════════
|
||
|
||
let logPollTimer=null;
|
||
|
||
/**
|
||
* Запустить поллинг логов.
|
||
* Работает только если элемент #log-panel существует и видим.
|
||
* Безопасен для повторного вызова — не дублирует таймер.
|
||
*/
|
||
function startLogPoll(){
|
||
if(logPollTimer) return; // уже запущен
|
||
logPollTimer=setInterval(async()=>{
|
||
const el=document.getElementById('log-panel');
|
||
// Если панель скрыта — не грузим (экономия трафика)
|
||
if(!el||el.style.display==='none') return;
|
||
try{
|
||
const r=await fetch('/api/log');
|
||
const lines=await r.json();
|
||
// Каждая строка логирования в отдельном div, экранирована
|
||
el.innerHTML=lines.map(l=>`<div>${_esc(l)}</div>`).join('');
|
||
// Автопрокрутка вниз (следим за новыми записями)
|
||
el.scrollTop=el.scrollHeight;
|
||
}catch(e){}
|
||
},2000);
|
||
}
|
||
|
||
/**
|
||
* Переключить видимость лог-панели.
|
||
* При открытии — запускает поллинг.
|
||
*/
|
||
function toggleLog(){
|
||
const el=document.getElementById('log-panel');
|
||
if(!el) return;
|
||
if(el.style.display==='none'){ el.style.display='block'; startLogPoll(); }
|
||
else el.style.display='none';
|
||
}
|
||
|
||
/**
|
||
* Относительное время — "5м", "2ч 30м", "1д 3ч".
|
||
* Используется в списке инстансов (instances.js) и истории (history.js).
|
||
*
|
||
* @param {string} iso — ISO-8601 дата (напр. "2026-07-31T12:00:00")
|
||
* @returns {string} относительное время или "-" если невалидно
|
||
*/
|
||
function relativeTime(iso) {
|
||
if (!iso) return '-';
|
||
const d = new Date(iso);
|
||
if (isNaN(d.getTime())) return '-';
|
||
const sec = Math.floor((Date.now() - d.getTime()) / 1000);
|
||
if (sec < 0) return 'только что'; // будущее (расхождение часов)
|
||
if (sec < 60) return sec + 'с';
|
||
const min = Math.floor(sec / 60);
|
||
if (min < 60) return min + 'м';
|
||
const hr = Math.floor(min / 60);
|
||
if (hr < 24) return hr + 'ч ' + (min % 60) + 'м';
|
||
const days = Math.floor(hr / 24);
|
||
return days + 'д ' + (hr % 24) + 'ч';
|
||
}
|