482 lines
16 KiB
JavaScript
482 lines
16 KiB
JavaScript
// Главный модуль приложения Console
|
||
// Содержит всю логику UI, модальные окна, API запросы
|
||
|
||
const API_BASE = window.location.pathname.startsWith('/console') ? '/console/api' : '/api';
|
||
|
||
const S = {
|
||
envs: [],
|
||
fns: [],
|
||
httpTriggers: [],
|
||
timeTriggers: [],
|
||
currentEdit: null,
|
||
currentInvoke: null
|
||
};
|
||
|
||
function authHeaders() {
|
||
return {
|
||
'X-Auth-Token': localStorage.getItem('auth_token') || '',
|
||
'X-Auth-Env': localStorage.getItem('auth_env') || 'test'
|
||
};
|
||
}
|
||
|
||
async function getJSON(url) {
|
||
// Функция перемещена в api.js
|
||
}
|
||
|
||
async function requestJSON(url, method, body) {
|
||
// Функция перемещена в api.js
|
||
}
|
||
|
||
function showStatus(message, kind) {
|
||
// Функция перемещена в ui.js
|
||
}
|
||
|
||
function clearStatus() {
|
||
// Функция перемещена в ui.js
|
||
}
|
||
|
||
function setText(id, value) {
|
||
// Функция перемещена в ui.js
|
||
}
|
||
|
||
function parseMethods(v) {
|
||
const items = String(v || '').split(',').map(s => s.trim().toUpperCase()).filter(Boolean);
|
||
return items.length ? Array.from(new Set(items)) : ['GET'];
|
||
}
|
||
|
||
function parseTimeout(v) {
|
||
var n = Number(String(v || '').trim());
|
||
if (!Number.isFinite(n) || n <= 0) return 60;
|
||
return Math.round(n);
|
||
}
|
||
|
||
function llmGeneratedWarning(lang) {
|
||
var text = 'Сделано LLM. Не доверяй, проверяй!';
|
||
switch (lang) {
|
||
case 'php':
|
||
return '// ' + text;
|
||
case 'nodejs':
|
||
case 'go':
|
||
return '// ' + text;
|
||
case 'ruby':
|
||
case 'python':
|
||
default:
|
||
return '# ' + text;
|
||
}
|
||
}
|
||
|
||
function addLLMWarning(code, lang) {
|
||
var body = String(code || '').replace(/^\s+/, '');
|
||
var warning = llmGeneratedWarning(lang);
|
||
if (!body) return warning;
|
||
if (body.indexOf(warning) === 0) return body;
|
||
if (lang === 'php' && body.indexOf('<?php') === 0) {
|
||
var rest = body.slice(5);
|
||
rest = rest.replace(/^\r?\n/, '');
|
||
return '<?php\n' + warning + '\n' + rest;
|
||
}
|
||
return warning + '\n' + body;
|
||
}
|
||
|
||
function httpTriggerByFn(fnName) {
|
||
// Функция перемещена в triggers.js
|
||
}
|
||
|
||
function timeTriggerByFn(fnName) {
|
||
// Функция перемещена в triggers.js
|
||
}
|
||
|
||
function toggleScheduleFields(prefix) {
|
||
// Функция перемещена в triggers.js
|
||
}
|
||
|
||
function schedulePayload(prefix) {
|
||
// Функция перемещена в triggers.js
|
||
}
|
||
|
||
async function syncScheduleForFunction(name, prefix) {
|
||
// Функция перемещена в triggers.js
|
||
}
|
||
|
||
function h(v) {
|
||
// Функция перемещена в ui.js
|
||
return String(v == null ? '' : v);
|
||
}
|
||
|
||
function formatTimestamp(ts) {
|
||
// Функция перемещена в ui.js
|
||
}
|
||
|
||
function timestampCell(ts) {
|
||
// Функция перемещена в ui.js
|
||
}
|
||
|
||
const LANG_TEMPLATES = {
|
||
// Константы перемещены в config.js
|
||
};
|
||
|
||
async function openEdit(name) {
|
||
try {
|
||
const fn = await getJSON(API_BASE + '/functions/' + encodeURIComponent(name));
|
||
S.currentEdit = fn;
|
||
document.getElementById('e-title').textContent = 'Редактирование: ' + name;
|
||
document.getElementById('e-name').value = name;
|
||
document.getElementById('e-env').value = fn.environment || '';
|
||
document.getElementById('e-entry').value = fn.entrypoint || '';
|
||
document.getElementById('e-timeout').value = String(fn.timeout || 60);
|
||
document.getElementById('e-code').value = fn.code || '';
|
||
var schedule = timeTriggerByFn(name);
|
||
document.getElementById('e-schedule-enabled').checked = !!schedule;
|
||
document.getElementById('e-cron').value = (schedule && schedule.spec && schedule.spec.cron) || '';
|
||
toggleScheduleFields('e');
|
||
var envName = (fn.environment || '').toLowerCase();
|
||
var lang = 'python';
|
||
if (envName.includes('node')) lang = 'nodejs';
|
||
else if (envName.includes('go')) lang = 'go';
|
||
else if (envName.includes('ruby')) lang = 'ruby';
|
||
else if (envName.includes('php')) lang = 'php';
|
||
document.getElementById('e-lang-hidden').value = lang;
|
||
var aiRes = document.getElementById('e-ai-result');
|
||
if (aiRes) { aiRes.style.display = 'none'; aiRes.textContent = ''; }
|
||
var warnEl = document.getElementById('e-tf-warn');
|
||
if (warnEl) {
|
||
var isTf = /^tf-/.test(name) || /go[-_]env/.test(fn.environment || '');
|
||
warnEl.style.display = isTf ? 'block' : 'none';
|
||
}
|
||
document.getElementById('edit-modal').classList.add('open');
|
||
} catch (e) {
|
||
showStatus('Ошибка загрузки функции: ' + e.message, 'err');
|
||
}
|
||
}
|
||
|
||
function closeEdit() {
|
||
document.getElementById('edit-modal').classList.remove('open');
|
||
S.currentEdit = null;
|
||
}
|
||
|
||
function openHelp() {
|
||
document.getElementById('help-modal').classList.add('open');
|
||
}
|
||
|
||
function closeHelp() {
|
||
document.getElementById('help-modal').classList.remove('open');
|
||
}
|
||
|
||
async function submitEdit() {
|
||
if (!S.currentEdit) return;
|
||
const btn = document.getElementById('e-submit');
|
||
btn.disabled = true;
|
||
const progress = startTimedStatus('Сохраняем код...', 'Сохранение кода...', explainDelay);
|
||
try {
|
||
const name = S.currentEdit.name;
|
||
await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name) + '/code', 'PUT', {
|
||
code: document.getElementById('e-code').value,
|
||
timeout: parseTimeout(document.getElementById('e-timeout').value)
|
||
});
|
||
|
||
await syncScheduleForFunction(name, 'e');
|
||
closeEdit();
|
||
progress.stop('Код обновлён: ' + name, 'ok');
|
||
await reloadAll();
|
||
} catch (e) {
|
||
progress.stop('Ошибка обновления: ' + e.message, 'err');
|
||
} finally {
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
function openInvoke(name) {
|
||
S.currentInvoke = name;
|
||
document.getElementById('i-title').textContent = 'Вызов: ' + name;
|
||
document.getElementById('i-resp').value = '';
|
||
document.getElementById('i-status').textContent = '';
|
||
document.getElementById('i-meta').style.display = 'none';
|
||
document.getElementById('i-meta').textContent = '';
|
||
document.getElementById('invoke-modal').classList.add('open');
|
||
}
|
||
|
||
function closeInvoke() {
|
||
document.getElementById('invoke-modal').classList.remove('open');
|
||
S.currentInvoke = null;
|
||
}
|
||
|
||
async function submitInvoke() {
|
||
if (!S.currentInvoke) return;
|
||
const btn = document.getElementById('i-submit');
|
||
const statusEl = document.getElementById('i-status');
|
||
const metaEl = document.getElementById('i-meta');
|
||
const respEl = document.getElementById('i-resp');
|
||
btn.disabled = true;
|
||
respEl.value = '';
|
||
metaEl.style.display = 'none';
|
||
metaEl.textContent = '';
|
||
let elapsed = 0;
|
||
statusEl.textContent = 'Вызов...';
|
||
const timer = setInterval(() => {
|
||
elapsed++;
|
||
if (elapsed < 5) {
|
||
statusEl.textContent = 'Вызов... ' + elapsed + 'с';
|
||
} else if (elapsed < 10) {
|
||
statusEl.textContent = '⏳ Возможен cold start — прогрев пула... ' + elapsed + 'с';
|
||
} else {
|
||
statusEl.textContent = '⏳ Возможны cold start или specialization... ' + elapsed + 'с';
|
||
}
|
||
}, 1000);
|
||
try {
|
||
const raw = document.getElementById('i-body').value.trim();
|
||
let parsed = {};
|
||
if (raw) parsed = JSON.parse(raw);
|
||
const result = await requestJSON(API_BASE + '/functions/' + encodeURIComponent(S.currentInvoke) + '/invoke', 'POST', parsed);
|
||
var latencyMs = Number(result.latency_ms || 0);
|
||
var measuredSeconds = latencyMs > 0 ? Math.max(1, Math.round(latencyMs / 1000)) : elapsed;
|
||
statusEl.textContent = '✓ Выполнено за ' + formatSeconds(measuredSeconds);
|
||
metaEl.style.display = 'block';
|
||
metaEl.textContent = [
|
||
'HTTP status: ' + (result.status || 'n/a'),
|
||
'Latency: ' + (latencyMs || 'n/a') + ' ms',
|
||
'Причина задержки: ' + explainDelay(measuredSeconds),
|
||
'Invoke URL: ' + (result.invoke_url || 'n/a')
|
||
].join('\n');
|
||
respEl.value = JSON.stringify(result, null, 2);
|
||
} catch (e) {
|
||
statusEl.textContent = '✗ Ошибка после ' + formatSeconds(elapsed);
|
||
metaEl.style.display = 'block';
|
||
metaEl.textContent = [
|
||
'Последняя ошибка: ' + e.message,
|
||
'Вероятная причина задержки: ' + explainDelay(elapsed)
|
||
].join('\n');
|
||
respEl.value = 'Ошибка вызова: ' + e.message;
|
||
} finally {
|
||
clearInterval(timer);
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
async function removeFn(name) {
|
||
var tfWarn = (/^tf-/.test(name)) ? '\n\n⚠️ Эта функция управляется Terraform. Удаление приведёт к рассинхронизации state!' : '';
|
||
if (!confirm('Удалить функцию ' + name + '?' + tfWarn)) return;
|
||
var progress = startTimedStatus('Удаляем функцию...', 'Удаление функции...', explainDelay);
|
||
try {
|
||
await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name), 'DELETE');
|
||
progress.stop('Функция удалена: ' + name, 'ok');
|
||
await reloadAll();
|
||
} catch (e) {
|
||
progress.stop('Ошибка удаления: ' + e.message, 'err');
|
||
}
|
||
}
|
||
|
||
async function reloadAll() {
|
||
try {
|
||
const [envs, pkgs, fns, http, times] = await Promise.all([
|
||
getJSON(API_BASE + '/environments'),
|
||
getJSON(API_BASE + '/packages'),
|
||
getJSON(API_BASE + '/functions'),
|
||
getJSON(API_BASE + '/httptriggers'),
|
||
getJSON(API_BASE + '/timetriggers')
|
||
]);
|
||
|
||
S.envs = envs || [];
|
||
S.fns = fns || [];
|
||
S.httpTriggers = http || [];
|
||
S.timeTriggers = times || [];
|
||
|
||
setText('env-count', envs.length || 0);
|
||
setText('pkg-count', pkgs.length || 0);
|
||
setText('fn-count', fns.length || 0);
|
||
setText('http-count', http.length || 0);
|
||
setText('cron-count', new Set((times || []).map(function (t) {
|
||
return t && t.spec && t.spec.functionref && t.spec.functionref.name;
|
||
}).filter(Boolean)).size);
|
||
|
||
const rows = (fns || []).map(function (f) {
|
||
const spec = f.spec || {};
|
||
const meta = f.metadata || {};
|
||
const ann = meta.annotations || {};
|
||
const env = (spec.environment && spec.environment.name) || '-';
|
||
const pkg = (spec.package && spec.package.packageref && spec.package.packageref.name) || '-';
|
||
const name = (f.metadata && f.metadata.name) || '-';
|
||
const trig = httpTriggerByFn(name) || {};
|
||
const timeTrig = timeTriggerByFn(name) || {};
|
||
const route = (trig.spec && trig.spec.relativeurl) || '-';
|
||
const methods = (trig.spec && trig.spec.methods) || [];
|
||
const chips = methods.map(function (m) { return '<span class="chip">' + h(m) + '</span>'; }).join('');
|
||
const cron = (timeTrig.spec && timeTrig.spec.cron) || '';
|
||
const cronCell = cron ? '<span class="chip">' + h(cron) + '</span>' : '<span class="mono" style="color:var(--text-secondary)">—</span>';
|
||
const createdAt = ann['fission-console/created-at'] || meta.creationTimestamp || '-';
|
||
const updatedAt = ann['fission-console/updated-at'] || createdAt;
|
||
var isGo = /go[-_]env/.test(env);
|
||
var isTf = /^tf-/.test(name);
|
||
var tfBadge = (isGo || isTf) ? '<span class="chip" style="background:#555;color:#ffa" title="Управляется Terraform. Изменения могут быть перезаписаны при terraform apply.">TF</span> ' : '';
|
||
var actions = tfBadge +
|
||
'<button class="btn ghost" onclick="openEdit(\'' + h(name) + '\')">\u0420\u0435\u0434.</button> ' +
|
||
'<button class="btn ghost" onclick="openInvoke(\'' + h(name) + '\')">\u0412\u044b\u0437\u043e\u0432</button> ' +
|
||
'<button class="btn danger" onclick="removeFn(\'' + h(name) + '\')">\u0423\u0434\u0430\u043b\u0438\u0442\u044c</button>';
|
||
return '<tr>' +
|
||
'<td class="mono">' + h(name) + '</td>' +
|
||
'<td>' + h(env) + '</td>' +
|
||
'<td class="mono">' + h(pkg) + '</td>' +
|
||
'<td>' + timestampCell(createdAt) + '</td>' +
|
||
'<td>' + timestampCell(updatedAt) + '</td>' +
|
||
'<td class="mono">' + h(route) + '</td>' +
|
||
'<td>' + chips + '</td>' +
|
||
'<td>' + cronCell + '</td>' +
|
||
'<td class="nowrap">' + actions + '</td>' +
|
||
'</tr>';
|
||
}).join('');
|
||
|
||
document.getElementById('fn-rows').innerHTML = rows || '<tr><td colspan="9">Нет функций</td></tr>';
|
||
} catch (e) {
|
||
document.getElementById('fn-rows').innerHTML = '<tr><td colspan="9">Load error: ' + e.message + '</td></tr>';
|
||
showStatus('Ошибка загрузки: ' + e.message, 'err');
|
||
}
|
||
}
|
||
|
||
// Инициализация и загрузка модулей
|
||
fetch('templates/modals/login.html')
|
||
.then(r => r.text())
|
||
.then(html => {
|
||
document.body.insertAdjacentHTML('afterbegin', html);
|
||
});
|
||
|
||
// Инициализация функций
|
||
async function aiCheck(codeId, langId, resultId) {
|
||
// Функция перемещена в ai.js — не удаляй, это placeholder для обратной совместимости
|
||
console.error('aiCheck должна быть вызвана из ai.js, но она не загружена!');
|
||
}
|
||
|
||
// Init polling (перемещено в init.js)
|
||
var _initPolling = false;
|
||
var _initTickBusy = false;
|
||
var _initStartedAt = 0;
|
||
|
||
function initLog(msg) {
|
||
// Функция перемещена в init.js
|
||
}
|
||
|
||
function formatSeconds(totalSeconds) {
|
||
// Функция перемещена в init.js
|
||
}
|
||
|
||
function setInitElapsed(seconds) {
|
||
// Функция перемещена в init.js
|
||
}
|
||
|
||
function setInitCurrentStage(text) {
|
||
// Функция перемещена в init.js
|
||
}
|
||
|
||
function setBox(id, text) {
|
||
// Функция перемещена в init.js
|
||
}
|
||
|
||
function missingKeys(obj) {
|
||
// Функция перемещена в init.js
|
||
}
|
||
|
||
function firstPendingStage(stages) {
|
||
// Функция перемещена в init.js
|
||
}
|
||
|
||
function describeInitReason(debug) {
|
||
// Функция перемещена в init.js
|
||
}
|
||
|
||
function summarizeInit(status, debug) {
|
||
// Функция перемещена в init.js
|
||
}
|
||
|
||
function explainDelay(seconds) {
|
||
// Функция перемещена в init.js
|
||
}
|
||
|
||
function startTimedStatus(startText, waitingPrefix, waitingHint) {
|
||
// Функция перемещена в init.js
|
||
}
|
||
|
||
function renderInitStages(stages) {
|
||
// Функция перемещена в init.js
|
||
}
|
||
|
||
async function pollNSStatus() {
|
||
// Функция перемещена в init.js
|
||
}
|
||
|
||
function checkAuth() {
|
||
var storedToken = localStorage.getItem('auth_token');
|
||
if (!storedToken) {
|
||
showLoginOverlay();
|
||
return;
|
||
}
|
||
setUserAvatar(storedToken);
|
||
hideLoginOverlay();
|
||
reloadAll();
|
||
}
|
||
|
||
// AI Ассистент (функции перемещены в ui.js)
|
||
var _assistantOpen = true;
|
||
function toggleAssistant() {
|
||
// Функция перемещена в ui.js
|
||
}
|
||
|
||
async function askAssistant() {
|
||
// Функция перемещена в ui.js
|
||
}
|
||
|
||
function showGenPrompt() {
|
||
// Функция перемещена в ai.js
|
||
}
|
||
|
||
async function aiGenerate() {
|
||
// Функция перемещена в ai.js
|
||
}
|
||
|
||
async function aiExplain(codeId, langId, resultId) {
|
||
// Функция перемещена в ai.js
|
||
}
|
||
|
||
// Event delegation для data-* атрибутов
|
||
function initializeEventListeners() {
|
||
document.addEventListener('click', function(e) {
|
||
const target = e.target.closest('[data-onclick]');
|
||
if (target) {
|
||
const handler = target.dataset.onclick;
|
||
try {
|
||
// Попытаемся вызвать как функцию или выполнить как код
|
||
eval(`${handler}`);
|
||
} catch (err) {
|
||
console.error('Error in onclick handler:', handler, err);
|
||
}
|
||
}
|
||
});
|
||
|
||
document.addEventListener('change', function(e) {
|
||
const target = e.target.closest('[data-onchange]');
|
||
if (target) {
|
||
const handler = target.dataset.onchange;
|
||
try {
|
||
eval(`${handler}`);
|
||
} catch (err) {
|
||
console.error('Error in onchange handler:', handler, err);
|
||
}
|
||
}
|
||
});
|
||
|
||
document.addEventListener('keydown', function(e) {
|
||
const target = e.target.closest('[data-onkeydown]');
|
||
if (target) {
|
||
const expr = target.dataset.onkeydown;
|
||
try {
|
||
// Выполняем выражение с доступом к event
|
||
eval(`(function(event) { ${expr} }).call(this, e)`);
|
||
} catch (err) {
|
||
console.error('Error in onkeydown handler:', err);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Инициализация приложения
|
||
initializeEventListeners();
|
||
checkAuth();
|
||
|