v1.3.27: fix Help modal — add -L flag, demo-login docs
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed index.html cron.html
|
||||
//go:embed index.html cron.html css js
|
||||
var content embed.FS
|
||||
|
||||
func Handler() http.Handler {
|
||||
|
||||
+30
-1219
File diff suppressed because it is too large
Load Diff
+59
-1
@@ -1,4 +1,32 @@
|
||||
// AI-функции (линтер, генерация, объяснение)
|
||||
/* ai.js — AI/LLM функции: проверка, генерация, объяснение, ассистент */
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async function aiCheck(codeId, langId, resultId) {
|
||||
var code = document.getElementById(codeId).value.trim();
|
||||
@@ -28,6 +56,36 @@ async function aiCheck(codeId, langId, resultId) {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function showGenPrompt() {
|
||||
var el = document.getElementById('c-gen-prompt');
|
||||
el.style.display = 'flex';
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
// API-вызовы и авторизация
|
||||
/* api.js — HTTP-утилиты */
|
||||
|
||||
function authHeaders() {
|
||||
return {
|
||||
'X-Auth-Token': localStorage.getItem('auth_token') || '',
|
||||
'X-Auth-Env': localStorage.getItem('auth_env') || 'test'
|
||||
};
|
||||
}
|
||||
|
||||
async function getJSON(url) {
|
||||
const r = await fetch(url, { headers: authHeaders() });
|
||||
|
||||
+7
-418
@@ -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();
|
||||
|
||||
|
||||
+31
-9
@@ -1,4 +1,4 @@
|
||||
// Логика авторизации (логин/логаут, токен, email)
|
||||
/* auth.js — авторизация */
|
||||
|
||||
function showLoginOverlay() {
|
||||
document.getElementById('login-overlay').style.display = 'flex';
|
||||
@@ -13,7 +13,9 @@ async function doLogin() {
|
||||
var errEl = document.getElementById('l-error');
|
||||
var token = (document.getElementById('l-token').value || '').trim();
|
||||
var env = document.getElementById('l-env').value;
|
||||
if (!token) { errEl.textContent = 'Введите токен'; errEl.style.display = 'block'; return; }
|
||||
if (!token) { errEl.textContent = 'Введите токен или логин'; errEl.style.display = 'block'; return; }
|
||||
var isJWT = token.split('.').length === 3 && !token.includes(' ');
|
||||
if (!isJWT && token.length < 6) { errEl.textContent = 'Логин — не менее 6 символов'; errEl.style.display = 'block'; return; }
|
||||
btn.disabled = true;
|
||||
errEl.style.display = 'none';
|
||||
try {
|
||||
@@ -26,8 +28,11 @@ async function doLogin() {
|
||||
const d = await res.json().catch(function () { return {}; });
|
||||
throw new Error(d.error || 'Ошибка входа');
|
||||
}
|
||||
const data = await res.json();
|
||||
localStorage.setItem('auth_token', token);
|
||||
localStorage.setItem('auth_env', env);
|
||||
if (data.email) localStorage.setItem('auth_email', data.email);
|
||||
else localStorage.removeItem('auth_email');
|
||||
setUserAvatar(token);
|
||||
hideLoginOverlay();
|
||||
// Проверяем статус NS — если не ready, показываем init overlay
|
||||
@@ -58,26 +63,43 @@ function jwtEmail(token) {
|
||||
function setUserAvatar(token) {
|
||||
var el = document.getElementById('user-avatar');
|
||||
if (!el) return;
|
||||
var email = token ? jwtEmail(token) : null;
|
||||
var email = localStorage.getItem('auth_email') || (token ? jwtEmail(token) : null);
|
||||
if (email) {
|
||||
el.textContent = email;
|
||||
el.title = email;
|
||||
el.style.fontSize = '0.7rem';
|
||||
el.style.minWidth = '60px';
|
||||
el.style.padding = '0 8px';
|
||||
el.style.fontSize = '0.75rem';
|
||||
el.style.minWidth = 'auto';
|
||||
el.style.padding = '0 10px';
|
||||
el.style.background = 'none';
|
||||
el.style.border = '1px solid var(--border)';
|
||||
el.style.borderRadius = '6px';
|
||||
el.style.color = 'var(--text-primary)';
|
||||
el.style.fontWeight = '400';
|
||||
el.style.width = 'auto';
|
||||
el.style.height = '30px';
|
||||
} else {
|
||||
el.textContent = 'N';
|
||||
el.title = '';
|
||||
el.style.fontSize = '';
|
||||
el.style.minWidth = '';
|
||||
el.style.padding = '';
|
||||
el.style.cssText = '';
|
||||
}
|
||||
}
|
||||
|
||||
function doLogout() {
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('auth_env');
|
||||
localStorage.removeItem('auth_email');
|
||||
try { document.getElementById('l-token').value = ''; } catch (_) { }
|
||||
setUserAvatar(null);
|
||||
showLoginOverlay();
|
||||
}
|
||||
|
||||
function checkAuth() {
|
||||
var storedToken = localStorage.getItem('auth_token');
|
||||
if (!storedToken) {
|
||||
showLoginOverlay();
|
||||
return;
|
||||
}
|
||||
setUserAvatar(storedToken);
|
||||
hideLoginOverlay();
|
||||
reloadAll();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Константы и настройки
|
||||
/* config.js — константы и шаблоны языков */
|
||||
|
||||
const LANG_TEMPLATES = {
|
||||
python: {
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
// CRUD-операции с функциями (создание, редактирование, удаление)
|
||||
/* functions.js — CRUD операции с функциями */
|
||||
|
||||
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 onLangChange() {
|
||||
var lang = document.getElementById('c-lang').value;
|
||||
@@ -56,11 +67,86 @@ async function submitCreate() {
|
||||
}
|
||||
|
||||
closeCreate();
|
||||
progress.stop('\u0424\u0443\u043d\u043a\u0446\u0438\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0430: ' + name, 'ok');
|
||||
progress.stop('Функция создана: ' + name, 'ok');
|
||||
await reloadAll();
|
||||
} catch (e) {
|
||||
progress.stop('\u041e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f: ' + e.message, 'err');
|
||||
progress.stop('Ошибка создания: ' + e.message, 'err');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Инициализация окружения
|
||||
/* init.js — инициализация namespace, утилиты таймера и статуса */
|
||||
|
||||
// Init polling
|
||||
var _initPolling = false;
|
||||
var _initTickBusy = false;
|
||||
var _initStartedAt = 0;
|
||||
@@ -136,7 +135,7 @@ async function pollNSStatus() {
|
||||
setBox('init-error', '');
|
||||
initLog('Запуск инициализации...');
|
||||
var attempt = 0;
|
||||
var maxAttempts = 60;
|
||||
var maxAttempts = 60; // 5 минут
|
||||
var interval = setInterval(async function () {
|
||||
if (_initTickBusy) return;
|
||||
_initTickBusy = true;
|
||||
|
||||
+1
-76
@@ -1,43 +1,4 @@
|
||||
// Модальные окна
|
||||
|
||||
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;
|
||||
}
|
||||
/* modals.js — управление модальными окнами Help и Invoke */
|
||||
|
||||
function openHelp() {
|
||||
document.getElementById('help-modal').classList.add('open');
|
||||
@@ -47,29 +8,6 @@ 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;
|
||||
@@ -136,16 +74,3 @@ async function submitInvoke() {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// HTTP и Cron триггеры
|
||||
/* triggers.js — HTTP и Cron триггеры */
|
||||
|
||||
function httpTriggerByFn(fnName) {
|
||||
return (S.httpTriggers || []).find(function (t) {
|
||||
|
||||
+5
-35
@@ -1,4 +1,8 @@
|
||||
// UI-вспомогательные функции
|
||||
/* ui.js — UI-вспомогательные функции */
|
||||
|
||||
function setText(id, value) {
|
||||
document.getElementById(id).textContent = String(value);
|
||||
}
|
||||
|
||||
function showStatus(message, kind) {
|
||||
const el = document.getElementById('status');
|
||||
@@ -14,10 +18,6 @@ function clearStatus() {
|
||||
el.textContent = '';
|
||||
}
|
||||
|
||||
function setText(id, value) {
|
||||
document.getElementById(id).textContent = String(value);
|
||||
}
|
||||
|
||||
function h(v) {
|
||||
return String(v == null ? '' : v)
|
||||
.replaceAll('&', '&')
|
||||
@@ -45,33 +45,3 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<!-- AI Assistant -->
|
||||
@@ -1 +0,0 @@
|
||||
<!-- Navbar -->
|
||||
@@ -1 +0,0 @@
|
||||
<!-- Status cards -->
|
||||
@@ -1 +0,0 @@
|
||||
<!-- Модальное окно создания функции -->
|
||||
@@ -1 +0,0 @@
|
||||
<!-- Модальное окно редактирования функции -->
|
||||
@@ -1 +0,0 @@
|
||||
<!-- Модальное окно помощи -->
|
||||
@@ -1 +0,0 @@
|
||||
<!-- Модальное окно инициализации -->
|
||||
@@ -1 +0,0 @@
|
||||
<!-- Модальное окно вызова функции -->
|
||||
@@ -1,32 +0,0 @@
|
||||
<!-- Модальное окно логина -->
|
||||
<div id="login-overlay"
|
||||
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.85); z-index:999; align-items:center; justify-content:center; padding:16px;">
|
||||
<div class="panel" style="width:min(440px,100%);">
|
||||
<div class="brand" style="margin-bottom:24px;">
|
||||
<div class="brand-mark">N</div>
|
||||
<div class="brand-text">
|
||||
<div class="nubes">NUBES</div>
|
||||
<div class="product">FISSION CONSOLE</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="flex-direction:column; gap:6px; margin-bottom:12px;">
|
||||
<label style="font-size:12px; color:#999;">Стенд</label>
|
||||
<select id="l-env"
|
||||
style="background:var(--bg-base); border:1px solid var(--border); color:var(--fg); padding:8px 10px; border-radius:6px;">
|
||||
<option value="dev">Dev</option>
|
||||
<option value="test" selected>Test</option>
|
||||
<option value="prod">Prod</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row" style="flex-direction:column; gap:6px; margin-bottom:12px;">
|
||||
<label style="font-size:12px; color:#999;">Токен</label>
|
||||
<textarea id="l-token" rows="5"
|
||||
style="background:var(--bg-base); border:1px solid var(--border); color:var(--fg); padding:8px 10px; border-radius:6px; font-family:monospace; font-size:12px; resize:vertical; width:100%; box-sizing:border-box;"
|
||||
placeholder="Введите токен..."></textarea>
|
||||
<div style="font-size:11px; color:var(--text-secondary); margin-top:4px;">Токен: <strong
|
||||
style="color:#8bc7ff;">Личный кабинет → Профиль пользователя → Токены</strong></div>
|
||||
</div>
|
||||
<div id="l-error" style="display:none; color:#ff6b6b; font-size:13px; margin-bottom:10px;"></div>
|
||||
<button id="l-btn" class="btn" style="width:100%;" onclick="doLogin()">Войти</button>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user