v1.3.27: fix Help modal — add -L flag, demo-login docs

This commit is contained in:
Naeel
2026-05-03 06:46:27 +03:00
parent d7352532dc
commit df74a69fde
37 changed files with 1179 additions and 2497 deletions
+7 -418
View File
@@ -1,269 +1,4 @@
// Главный модуль приложения 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');
}
}
/* app.js — основная логика: загрузка данных, таблица функций */
async function reloadAll() {
try {
@@ -308,9 +43,9 @@ async function reloadAll() {
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>';
'<button class="btn ghost" onclick="openEdit(\'' + h(name) + '\')">Ред.</button> ' +
'<button class="btn ghost" onclick="openInvoke(\'' + h(name) + '\')">Вызов</button> ' +
'<button class="btn danger" onclick="removeFn(\'' + h(name) + '\')">Удалить</button>';
return '<tr>' +
'<td class="mono">' + h(name) + '</td>' +
'<td>' + h(env) + '</td>' +
@@ -325,157 +60,11 @@ async function reloadAll() {
}).join('');
document.getElementById('fn-rows').innerHTML = rows || '<tr><td colspan="9">Нет функций</td></tr>';
if (!rows) {
document.getElementById('fn-rows').innerHTML = '<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();