Files

112 lines
5.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// utils.js — общие хелперы фронтенда
// Используется ВСЕМИ модулями (должен грузиться ПЕРВЫМ).
// Экспортирует (в глобальную область): _esc, validateJson, relativeTime,
// startLogPoll, toggleLog
/**
* HTML-экранирование — защита от XSS.
* Все данные из API/БД пропускаются через _esc() перед вставкой в innerHTML.
*
* @param {*} s — что угодно (приводится к строке через String())
* @returns {string} безопасная для HTML строка
*
* Заменяет:
* & → & (первым! иначе сломает уже заменённые сущности)
* " → " (атрибуты в кавычках)
* < → &lt; (XSS-вектор: <script>)
*
* > и ' НЕ экранируем — в нашем контексте они безопасны,
* а лишние замены портят читаемость.
*/
function _esc(s){
return String(s||'').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;');
}
/**
* Проверить что значение в поле — валидный 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) + 'ч';
}