chore: sync all changes from feat/ui-archive-upload session

This commit is contained in:
“Naeel”
2026-05-02 16:50:44 +04:00
parent aea4e93ca1
commit d7352532dc
15 changed files with 2933 additions and 19 deletions
+90
View File
@@ -1 +1,91 @@
// AI-функции (линтер, генерация, объяснение)
async function aiCheck(codeId, langId, resultId) {
var code = document.getElementById(codeId).value.trim();
var lang = (document.getElementById(langId) ? document.getElementById(langId).value : '') || 'python';
var resEl = document.getElementById(resultId);
if (!code) { resEl.style.display = 'block'; resEl.style.background = 'var(--bg-alt)'; resEl.textContent = 'Введите код для проверки.'; return; }
var btnId = codeId === 'c-code' ? 'c-ai-btn' : 'e-ai-btn';
var btn = document.getElementById(btnId);
btn.disabled = true;
btn.textContent = '⏳ Проверяю...';
resEl.style.display = 'block';
resEl.style.background = 'var(--bg-alt)';
resEl.style.color = 'var(--fg)';
resEl.textContent = 'Проверяю код линтером...';
try {
var data = await requestJSON(API_BASE + '/ai/check', 'POST', { language: lang, code: code });
resEl.style.background = data.ok ? '#1a3a1a' : '#3a1a1a';
resEl.style.color = data.ok ? '#8f8' : '#f88';
resEl.textContent = data.result || '(пустой ответ)';
} catch (e) {
resEl.style.background = '#3a2a00';
resEl.style.color = '#ffa';
resEl.textContent = 'Ошибка: ' + e.message;
} finally {
btn.disabled = false;
btn.textContent = '🔍 Проверить синтаксис линтером';
}
}
function showGenPrompt() {
var el = document.getElementById('c-gen-prompt');
el.style.display = 'flex';
document.getElementById('c-gen-desc').focus();
}
async function aiGenerate() {
var name = (document.getElementById('c-name').value || '').trim() || 'my-func';
var lang = document.getElementById('c-lang').value || 'python';
var desc = (document.getElementById('c-gen-desc').value || '').trim();
if (!desc) { document.getElementById('c-gen-desc').focus(); return; }
var btn = document.getElementById('c-gen-btn');
var resEl = document.getElementById('c-ai-result');
btn.disabled = true; btn.textContent = '⏳ Генерирую...';
resEl.style.display = 'block'; resEl.style.background = 'var(--bg-alt)'; resEl.style.color = 'var(--fg)';
resEl.textContent = 'Запрашиваю у LLM...';
try {
var d = await requestJSON('/console/api/ai/ask', 'POST', {
mode: 'codegen',
language: lang,
name: name,
description: desc
});
var code = (d.answer || '').replace(/^```[\w]*\n?/, '').replace(/\n?```$/, '');
document.getElementById('c-code').value = addLLMWarning(code, lang);
document.getElementById('c-gen-prompt').style.display = 'none';
resEl.style.background = '#1a3a1a'; resEl.style.color = '#8f8';
resEl.textContent = 'Код сгенерирован и вставлен.';
} catch (e) {
resEl.style.background = '#3a2a00'; resEl.style.color = '#ffa';
resEl.textContent = 'Ошибка: ' + e.message;
} finally {
btn.disabled = false; btn.textContent = '✨ Сгенерировать код LLM';
}
}
async function aiExplain(codeId, langId, resultId) {
var code = (document.getElementById(codeId).value || '').trim();
var lang = (document.getElementById(langId) ? document.getElementById(langId).value : '') || 'python';
var resEl = document.getElementById(resultId);
var btnId = codeId === 'c-code' ? 'c-exp-btn' : 'e-exp-btn';
var btn = document.getElementById(btnId);
if (!code) { resEl.style.display = 'block'; resEl.style.background = 'var(--bg-alt)'; resEl.textContent = 'Введите код.'; return; }
btn.disabled = true; btn.textContent = '⏳ Объясняю...';
resEl.style.display = 'block'; resEl.style.background = 'var(--bg-alt)'; resEl.style.color = 'var(--fg)';
resEl.textContent = 'Запрашиваю у LLM...';
try {
var d = await requestJSON('/console/api/ai/ask', 'POST', {
mode: 'explain',
language: lang,
code: code
});
resEl.style.background = '#1a2a3a'; resEl.style.color = '#8cf';
resEl.textContent = d.answer || '(пустой ответ)';
} catch (e) {
resEl.style.background = '#3a2a00'; resEl.style.color = '#ffa';
resEl.textContent = 'Ошибка: ' + e.message;
} finally {
btn.disabled = false; btn.textContent = '📖 LLM: Что делает?';
}
}
+31
View File
@@ -1 +1,32 @@
// API-вызовы и авторизация
async function getJSON(url) {
const r = await fetch(url, { headers: authHeaders() });
if (r.status === 401) { doLogout(); throw new Error('Сессия истекла'); }
if (!r.ok) {
let msg = '';
try {
const body = await r.json();
msg = body.error || JSON.stringify(body);
} catch (_) {
msg = await r.text();
}
throw new Error(url + ': ' + r.status + (msg ? ' - ' + msg : ''));
}
return r.json();
}
async function requestJSON(url, method, body) {
const r = await fetch(url, {
method: method,
headers: Object.assign({ 'Content-Type': 'application/json' }, authHeaders()),
body: body ? JSON.stringify(body) : undefined
});
if (r.status === 401) { doLogout(); throw new Error('Сессия истекла'); }
let data = {};
try { data = await r.json(); } catch (_) { }
if (!r.ok) {
throw new Error(data.error || (url + ': ' + r.status));
}
return data;
}
+481 -1
View File
@@ -1 +1,481 @@
// Главный контроллер приложения
// Главный модуль приложения 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();
+19
View File
@@ -1 +1,20 @@
// Константы и настройки
const LANG_TEMPLATES = {
python: {
entrypoint: 'main.main',
code: 'def main():\n return {"ok": True}'
},
nodejs: {
entrypoint: 'main',
code: 'module.exports = async function(context) {\n return {\n status: 200,\n body: "hello from fission"\n };\n}'
},
php: {
entrypoint: 'main.php::handler',
code: '<?php\nfunction handler($context)\n{\n $response = $context["response"];\n $response->getBody()->write("hello from fission");\n}'
},
ruby: {
entrypoint: 'handler',
code: '# frozen_string_literal: true\n\ndef handler\n "hello from fission"\nend'
}
};
+180
View File
@@ -1 +1,181 @@
// Инициализация окружения
// Init polling
var _initPolling = false;
var _initTickBusy = false;
var _initStartedAt = 0;
function initLog(msg) {
var el = document.getElementById('init-log');
var ts = new Date().toLocaleTimeString('ru-RU');
el.textContent += '[' + ts + '] ' + msg + '\n';
el.scrollTop = el.scrollHeight;
}
function formatSeconds(totalSeconds) {
totalSeconds = Math.max(0, Number(totalSeconds) || 0);
var minutes = Math.floor(totalSeconds / 60);
var seconds = totalSeconds % 60;
if (minutes <= 0) return seconds + 'с';
return minutes + 'м ' + seconds + 'с';
}
function setInitElapsed(seconds) {
document.getElementById('init-elapsed').textContent = formatSeconds(seconds);
}
function setInitCurrentStage(text) {
document.getElementById('init-current-stage').textContent = text || 'Ожидание старта';
}
function setBox(id, text) {
var el = document.getElementById(id);
if (!el) return;
if (!text) {
el.style.display = 'none';
el.textContent = '';
return;
}
el.style.display = 'block';
el.textContent = text;
}
function missingKeys(obj) {
return Object.keys(obj || {}).filter(function (key) {
return !obj[key];
});
}
function firstPendingStage(stages) {
var list = stages || [];
for (var i = 0; i < list.length; i++) {
if (!list[i].done) return list[i].name;
}
return list.length ? list[list.length - 1].name : 'Ожидание старта';
}
function describeInitReason(debug) {
if (!debug) return 'Собираем диагностические данные по namespace и control-plane...';
if (!debug.exists) return 'Ещё не создан Kubernetes namespace пользователя.';
if (!debug.active) return 'Namespace создан, но Kubernetes ещё не перевёл его в состояние Active.';
var preparation = debug.preparation || {};
if (!preparation.managedByConsole) return 'Namespace существует, но ещё не помечен как managed-by=fission-console.';
if (!preparation.watcherLabel) return 'Namespace ещё не помечен label fission.io/managed=true для NSWatcher.';
var missingSAs = missingKeys(preparation.serviceAccounts);
if (missingSAs.length) return 'Не готовы bootstrap service accounts: ' + missingSAs.join(', ') + '.';
var missingRBs = missingKeys(preparation.roleBindings);
if (missingRBs.length) return 'Не готовы bootstrap role bindings: ' + missingRBs.join(', ') + '.';
var control = debug.controlPlane || {};
if (!control.executor || !control.executor.ready) {
return 'Executor ещё не ready. Возможна задержка из-за cold start, image pull или specialization.';
}
if (!control.router || !control.router.ready) {
return 'Router ещё не ready. HTTP маршрутизация пока не активна.';
}
return 'Ожидаем финальное подтверждение готовности control-plane.';
}
function summarizeInit(status, debug) {
var done = status && status.stages ? status.stages.filter(function (s) { return s.done; }).length : 0;
var namespace = debug && debug.namespace ? debug.namespace : 'не определён';
return 'Namespace: ' + namespace + '. Завершено шагов: ' + done + '/3.';
}
function explainDelay(seconds) {
if (seconds < 5) return 'Обычная операция без заметной задержки.';
if (seconds < 10) return 'Вероятен cold start: прогрев пула, image pull или ожидание specialization.';
return 'Задержка выше нормы: возможны cold start, specialization или ожидание готовности control-plane.';
}
function startTimedStatus(startText, waitingPrefix, waitingHint) {
var startedAt = Date.now();
showStatus(startText, '');
var timer = setInterval(function () {
var elapsed = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
var text = waitingPrefix + ' ' + formatSeconds(elapsed);
if (waitingHint) text += ' · ' + waitingHint(elapsed);
showStatus(text, '');
}, 1000);
return {
stop: function (message, kind) {
clearInterval(timer);
var elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
var text = message;
if (message) text += ' за ' + formatSeconds(elapsed);
showStatus(text, kind || '');
}
};
}
function renderInitStages(stages) {
var el = document.getElementById('init-stages');
el.innerHTML = stages.map(function (s) {
var icon = s.done ? '✅' : '⏳';
var color = s.done ? '#4caf50' : '#8bc7ff';
return '<div style="font-size:13px; color:' + color + ';">' + icon + '&nbsp;' + s.name + '</div>';
}).join('');
}
async function pollNSStatus() {
if (_initPolling) return;
_initPolling = true;
_initTickBusy = false;
_initStartedAt = Date.now();
var overlay = document.getElementById('init-overlay');
overlay.style.display = 'flex';
document.getElementById('init-log').textContent = '';
setInitElapsed(0);
setInitCurrentStage('Подготовка запуска');
setBox('init-summary', 'Проверяем состояние namespace и control-plane...');
setBox('init-reason', 'Если пользователь новый, возможны cold start и создание bootstrap ресурсов.');
setBox('init-error', '');
initLog('Запуск инициализации...');
var attempt = 0;
var maxAttempts = 60;
var interval = setInterval(async function () {
if (_initTickBusy) return;
_initTickBusy = true;
attempt++;
try {
var elapsed = Math.floor((Date.now() - _initStartedAt) / 1000);
setInitElapsed(elapsed);
var statusResp = await fetch(API_BASE + '/ns/status', { headers: authHeaders() });
var debugResp = await fetch(API_BASE + '/ns/debug', { headers: authHeaders() });
var d = await statusResp.json();
var dbg = await debugResp.json().catch(function () { return null; });
if (d.stages) renderInitStages(d.stages);
setInitCurrentStage(firstPendingStage(d.stages));
setBox('init-summary', summarizeInit(d, dbg));
setBox('init-reason', describeInitReason(dbg));
setBox('init-error', '');
var done = d.stages ? d.stages.filter(function (s) { return s.done; }).length : 0;
initLog('Шагов завершено: ' + done + '/3');
if (d.ready) {
initLog('✅ Готово! Загружаем консоль...');
clearInterval(interval);
_initPolling = false;
_initTickBusy = false;
overlay.style.display = 'none';
reloadAll();
return;
}
} catch (e) {
setBox('init-error', 'Последняя ошибка: ' + e.message);
initLog('Ошибка опроса: ' + e.message);
} finally {
_initTickBusy = false;
}
if (attempt >= maxAttempts) {
clearInterval(interval);
_initPolling = false;
_initTickBusy = false;
setBox('init-error', 'Превышено время ожидания. Чаще всего это bootstrap RBAC, control-plane readiness или холодный старт.');
initLog('⚠️ Превышено время ожидания. Попробуйте обновить страницу.');
}
}, 5000);
}
+150
View File
@@ -1 +1,151 @@
// Модальные окна
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');
}
}
+59
View File
@@ -1 +1,60 @@
// HTTP и Cron триггеры
function httpTriggerByFn(fnName) {
return (S.httpTriggers || []).find(function (t) {
return t.spec && t.spec.functionref && t.spec.functionref.name === fnName;
});
}
function timeTriggerByFn(fnName) {
return (S.timeTriggers || []).find(function (t) {
return t.spec && t.spec.functionref && t.spec.functionref.name === fnName;
});
}
function toggleScheduleFields(prefix) {
var enabled = !!document.getElementById(prefix + '-schedule-enabled').checked;
var cronEl = document.getElementById(prefix + '-cron');
cronEl.disabled = !enabled;
if (enabled && !cronEl.value.trim()) {
cronEl.value = '*/5 * * * *';
}
}
function schedulePayload(prefix) {
return {
enabled: !!document.getElementById(prefix + '-schedule-enabled').checked,
cron: (document.getElementById(prefix + '-cron').value || '').trim()
};
}
async function syncScheduleForFunction(name, prefix) {
var schedule = schedulePayload(prefix);
var existing = timeTriggerByFn(name);
var existingName = (existing && existing.metadata && existing.metadata.name) || name;
if (!schedule.enabled) {
if (existing) {
await requestJSON(API_BASE + '/timetriggers/' + encodeURIComponent(existingName), 'DELETE');
}
return;
}
if (!schedule.cron) {
throw new Error('cron is required');
}
var payload = {
name: existingName,
functionName: name,
cron: schedule.cron,
method: 'POST',
subpath: '/'
};
if (existing) {
await requestJSON(API_BASE + '/timetriggers/' + encodeURIComponent(existingName), 'PUT', payload);
} else {
await requestJSON(API_BASE + '/timetriggers', 'POST', payload);
}
}
+76
View File
@@ -1 +1,77 @@
// UI-вспомогательные функции
function showStatus(message, kind) {
const el = document.getElementById('status');
el.style.display = 'block';
el.className = 'status ' + (kind || '');
el.textContent = message;
}
function clearStatus() {
const el = document.getElementById('status');
el.style.display = 'none';
el.className = 'status';
el.textContent = '';
}
function setText(id, value) {
document.getElementById(id).textContent = String(value);
}
function h(v) {
return String(v == null ? '' : v)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
function formatTimestamp(ts) {
if (!ts) return '—';
var d = new Date(ts);
if (isNaN(d.getTime())) return String(ts);
return d.toLocaleString('ru-RU', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
function timestampCell(ts) {
var text = formatTimestamp(ts);
return '<span class="mono" title="' + h(ts || '') + '">' + h(text) + '</span>';
}
// AI Ассистент
var _assistantOpen = true;
function toggleAssistant() {
_assistantOpen = !_assistantOpen;
document.getElementById('assistant-body').style.display = _assistantOpen ? 'flex' : 'none';
document.getElementById('assistant-toggle-icon').textContent = _assistantOpen ? '▲' : '▼';
}
async function askAssistant() {
var inp = document.getElementById('assistant-input');
var msgs = document.getElementById('assistant-messages');
var q = inp.value.trim();
if (!q) return;
inp.value = '';
msgs.textContent += '\n\n👤 ' + q + '\n⏳ ...';
msgs.scrollTop = msgs.scrollHeight;
try {
var r = await fetch('/console/api/ai/ask', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...authHeaders() },
body: JSON.stringify({ mode: 'chat', question: q })
});
var d = await r.json();
msgs.textContent = msgs.textContent.replace('⏳ ...', '🤖 ' + (d.answer || d.error || 'Нет ответа'));
} catch (e) {
msgs.textContent = msgs.textContent.replace('⏳ ...', '❌ Ошибка: ' + e.message);
}
msgs.scrollTop = msgs.scrollHeight;
}