Files
fission-console/console/ui/index.html
T

1008 lines
39 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>NUBES Fission Console</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAA4ZJREFUeJztm8+Lm0UYxz/fySZUoWIrVtbmXfyBP05Fa3e3rogsglJRK0p7E2svnrz6B3gQ8eRJFooXET14KKJ4FUQo3UQQ9CRC6W4WQW0VXZduTd7HQ9c22bzbTCY/ZpO8n0syb56Z+c437zPvhMzIzJhkXGwBsckNiC0gNrkBsQXEZsonSJLj4GNPgb2ACo9j9gDOvrSV6qkB6+sJ6WSJ5MIxUo4h5sDuI9U7tlZ57/+YTAN0zyOPUi+9jOxJxMOUj9wJOBBg117QvqGMwhNpcQ/J+rOkPIc0j+xeytyGsaWXa2+kvc312gxQMleHYgFtrQ9GYJmgmSfup3z15xuDNW/dGXOAjd680NgshlYdvcH2mfEwQIU0tOp4GNADuQGxBcRmPAyQgh/W42FAD+QGxBYQm/EwIF8HhJMbEFtAX3DreQqEkhsQW0BsxsMAlz8Gg8kNiC0gNuNhgJvK54BQcgNiC+gLhUlPgcuTbkAP5AbEFhCbcAOM4LyLitIW3eEGyP7qWUwMHFdaixNO1v4Av5pG8H/yfWfvesk7tq5GczHDAHnmtu727nTQbJYe8o51+qOl2BYgNv1a0iHvTgeNGq94x6buYnOx3YCU3/1asn06ePQZ744HiXTCO3a6/m1zsd0Ax/f+HTc+kU76598AUDL/LpjfjjWxYdXqRvOljDvAfezfO3dQvvCDtLjHu04f0czsaSx9y7uC8eP2S20GWO38Z+A7DwDwIOX135TMvylpKI9VJQv7lcx+hfEhurELsCPOfdDWVtZ5ASWzZ4GXArQ1QJfA/kTW6BhtLgX+7qL9IqTTmKa7GjgAumKry7dsv5q9Vfbfq29QLL1I9wulAtgB4ADmoy9kY4fodugAODuT2dpOJ0aUzH8E6asBXe0+zP5h7bvbzay+/aOdv+Ha8mnE5YEKGxqF17MGDzcxwMzqTBUWgc65vKuxT7cm9kx2TIHrATOzxzHOEpZ5sanYamXuZgEdJzlbqXyOmzrO6N0J56hVj3YK8prl7eK5L6g3DmFc6l3XEDCWbLWyYGYdf9h1TIGWYMmRHDlDaqcY0qKnO/Qr4oStLH/jXSPk4KSShf2ovoTZ80Db4iICv2D2ttWqS91WDDKgpYHy4adR8TWww8BdmN2KrOg5Zwoo+PV0">
<script>
if (window.location.protocol !== 'https:' && window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
window.location.replace('https://' + window.location.host + window.location.pathname + window.location.search + window.location.hash);
}
</script>
<style>
:root {
--bg-page: #001120;
--bg-surface: #001929;
--bg-navbar: #001c34;
--border: #0b2d50;
--accent: #1a7fd4;
--text-primary: #e2ecf6;
--text-secondary: #6b8eaa;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
background: var(--bg-page);
color: var(--text-primary);
min-height: 100vh;
}
.navbar {
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
background: var(--bg-navbar);
border-bottom: 1px solid var(--border);
}
.title {
font-weight: 700;
letter-spacing: .3px;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
}
.brand-mark {
width: 30px;
height: 30px;
border-radius: 8px;
background: linear-gradient(135deg, #009dff 0%, #0055d4 100%);
color: #fff;
display: grid;
place-items: center;
font-size: 14px;
font-weight: 800;
letter-spacing: .02em;
box-shadow: 0 6px 18px rgba(0, 125, 255, .35);
}
.brand-text {
display: flex;
flex-direction: column;
line-height: 1.08;
}
.brand-text .nubes {
font-size: 12px;
letter-spacing: .22em;
font-weight: 800;
color: #8bc7ff;
}
.brand-text .product {
font-size: 13px;
font-weight: 700;
letter-spacing: .04em;
}
.btn {
background: var(--accent);
color: #fff;
border: 0;
border-radius: 6px;
padding: 8px 12px;
cursor: pointer;
font-size: 13px;
line-height: 1.2;
}
.btn:disabled {
opacity: .6;
cursor: not-allowed;
}
.btn.danger {
background: #b43232;
}
.btn.ghost {
background: #0c2b49;
}
.wrap {
max-width: 1100px;
margin: 0 auto;
padding: 20px;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px;
}
.k {
color: var(--text-secondary);
font-size: 12px;
text-transform: uppercase;
letter-spacing: .06em;
}
.v {
font-size: 24px;
font-weight: 700;
margin-top: 6px;
}
.box {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px;
margin-top: 12px;
}
.toolbar {
display: flex;
justify-content: space-between;
gap: 10px;
align-items: center;
margin-bottom: 10px;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
margin-top: 8px;
}
th, td {
text-align: left;
padding: 8px;
border-bottom: 1px solid #0b2a48;
}
.hint {
color: var(--text-secondary);
font-size: 12px;
margin-top: 8px;
}
.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 12px;
}
.status {
margin-top: 10px;
padding: 9px 10px;
border-radius: 6px;
font-size: 13px;
background: #0c2b49;
border: 1px solid #1d486d;
display: none;
}
.status.ok {
background: #103625;
border-color: #236843;
}
.status.err {
background: #411d1d;
border-color: #7a2f2f;
}
.chip {
display: inline-block;
padding: 2px 7px;
border-radius: 999px;
border: 1px solid #1d486d;
background: #0c2b49;
font-size: 11px;
margin-right: 4px;
}
.modal {
position: fixed;
inset: 0;
background: rgba(0,0,0,.45);
display: none;
align-items: center;
justify-content: center;
padding: 16px;
}
.modal.open { display: flex; }
.panel {
width: min(820px, 100%);
max-height: 90vh;
overflow: auto;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px;
}
.panel h3 {
margin-bottom: 10px;
font-size: 16px;
}
.row {
display: flex;
gap: 10px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.field {
flex: 1;
min-width: 180px;
}
label {
display: block;
margin-bottom: 4px;
font-size: 12px;
color: var(--text-secondary);
}
input, select, textarea {
width: 100%;
border: 1px solid #1d486d;
background: #03192c;
color: var(--text-primary);
border-radius: 6px;
padding: 8px 10px;
font-size: 13px;
}
textarea {
min-height: 220px;
resize: vertical;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.actions {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 8px;
flex-wrap: wrap;
}
.nowrap {
white-space: nowrap;
}
</style>
</head>
<body>
<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>
<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>
<div class="navbar">
<div class="brand">
<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="margin:0;">
<button class="btn ghost" onclick="reloadAll()">Refresh</button>
<button class="btn" onclick="openCreate()">+ Создать функцию</button>
<button class="btn ghost" onclick="doLogout()" style="margin-left:8px;">Выход</button>
</div>
</div>
<div class="wrap">
<div class="grid">
<div class="card"><div class="k">Окружения</div><div id="env-count" class="v">-</div></div>
<div class="card"><div class="k">Пакеты</div><div id="pkg-count" class="v">-</div></div>
<div class="card"><div class="k">Функции</div><div id="fn-count" class="v">-</div></div>
<div class="card"><div class="k">HTTP-триггеры</div><div id="http-count" class="v">-</div></div>
<div class="card"><div class="k">Тайм-триггеры</div><div id="time-count" class="v">-</div></div>
</div>
<div class="box">
<div class="toolbar">
<div style="font-weight:600;">Функции</div>
<div class="hint">Actions: view, edit code, invoke, delete</div>
</div>
<table>
<thead>
<tr><th>Имя</th><th>Окружение</th><th>Пакет</th><th>Маршрут</th><th>Методы</th><th class="nowrap">Действия</th></tr>
</thead>
<tbody id="fn-rows"></tbody>
</table>
<div id="status" class="status"></div>
<div class="hint">Namespace: default. CRUD происходит напрямую через CRD Fission.</div>
</div>
</div>
<div id="create-modal" class="modal">
<div class="panel">
<h3>Создать функцию</h3>
<div class="row">
<div class="field">
<label>Name</label>
<input id="c-name" placeholder="demo-fn">
</div>
<div class="field">
<label>Language</label>
<select id="c-lang" onchange="onLangChange()">
<option value="python">Python</option>
<option value="nodejs">Node.js</option>
<option value="php">PHP</option>
<option value="ruby">Ruby</option>
<option value="perl">Perl</option>
</select>
</div>
<div class="field">
<label>Entrypoint</label>
<input id="c-entry" value="main.main">
</div>
</div>
<div class="row">
<div class="field">
<label>Route</label>
<input id="c-route" placeholder="/demo-fn">
</div>
<div class="field">
<label>Методы (через запятую)</label>
<input id="c-methods" value="GET">
</div>
</div>
<div>
<label>Код</label>
<textarea id="c-code">def main(ctx):
return {"ok": True, "msg": "hello from fission console"}
</textarea>
<div style="margin-top:6px; display:flex; gap:6px; flex-wrap:wrap;">
<button class="btn ghost" id="c-ai-btn" onclick="aiCheck('c-code','c-lang','c-ai-result')">&#x1F50D; Проверить синтаксис</button>
<button class="btn ghost" id="c-gen-btn" onclick="showGenPrompt()">&#x2728; Сгенерировать код</button>
<button class="btn ghost" id="c-exp-btn" onclick="aiExplain('c-code','c-lang','c-ai-result')">&#x1F4D6; Что делает?</button>
</div>
<div id="c-gen-prompt" style="display:none; margin-top:8px; display:none; gap:6px; align-items:center;">
<input id="c-gen-desc" type="text" placeholder="Что должна делать функция? (напр: принять JSON, вернуть сумму чисел)"
style="flex:1; background:#2a2a42; border:1px solid #3a3a5c; border-radius:4px;
color:#cdd6f4; padding:6px 8px; font-size:.82rem; outline:none; width:100%;"
onkeydown="if(event.key==='Enter')aiGenerate()" />
<button class="btn" onclick="aiGenerate()" style="white-space:nowrap;">▶ Генерировать</button>
</div>
<div id="c-ai-result" style="display:none;margin-top:8px;padding:10px 12px;border-radius:6px;font-size:13px;line-height:1.5;white-space:pre-wrap;font-family:monospace;"></div>
</div>
<div class="actions">
<button class="btn ghost" onclick="closeCreate()">Отмена</button>
<button id="c-submit" class="btn" onclick="submitCreate()">Создать</button>
</div>
</div>
</div>
<div id="edit-modal" class="modal">
<div class="panel">
<h3 id="e-title">Редактирование кода</h3>
<div id="e-tf-warn" style="display:none;background:#553300;color:#ffa;padding:8px 12px;border-radius:6px;margin-bottom:10px;font-size:13px;">&#x26a0;&#xfe0f; Эта функция управляется Terraform. Изменения могут быть перезаписаны при следующем <code>terraform apply</code>.</div>
<input type="hidden" id="e-lang-hidden" value="">
<div class="row">
<div class="field">
<label>Name</label>
<input id="e-name" disabled>
</div>
<div class="field">
<label>Environment</label>
<input id="e-env" disabled>
</div>
<div class="field">
<label>Entrypoint</label>
<input id="e-entry" disabled>
</div>
</div>
<div>
<label>Код</label>
<textarea id="e-code"></textarea>
<div style="margin-top:6px; display:flex; gap:6px; flex-wrap:wrap;">
<button class="btn ghost" id="e-ai-btn" onclick="aiCheck('e-code','e-lang-hidden','e-ai-result')">&#x1F50D; Проверить синтаксис</button>
<button class="btn ghost" id="e-exp-btn" onclick="aiExplain('e-code','e-lang-hidden','e-ai-result')">&#x1F4D6; Что делает?</button>
</div>
<div id="e-ai-result" style="display:none;margin-top:8px;padding:10px 12px;border-radius:6px;font-size:13px;line-height:1.5;white-space:pre-wrap;font-family:monospace;"></div>
</div>
<div class="actions">
<button class="btn ghost" onclick="closeEdit()">Отмена</button>
<button id="e-submit" class="btn" onclick="submitEdit()">Сохранить</button>
</div>
</div>
</div>
<div id="invoke-modal" class="modal">
<div class="panel">
<h3 id="i-title">Вызов</h3>
<div>
<label>JSON тело запроса</label>
<textarea id="i-body">{"name":"world"}</textarea>
</div>
<div class="actions">
<button class="btn ghost" onclick="closeInvoke()">Отмена</button>
<button id="i-submit" class="btn" onclick="submitInvoke()">Вызвать</button>
</div>
<div id="i-status" style="margin-top:8px;font-size:13px;color:var(--text-secondary);min-height:20px;"></div>
<div style="margin-top:6px;">
<label>Response</label>
<textarea id="i-resp" readonly style="min-height:160px;"></textarea>
</div>
</div>
</div>
<script>
const API_BASE = window.location.pathname.startsWith('/console') ? '/console/api' : '/api';
const S = {
envs: [],
fns: [],
triggers: [],
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) {
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;
}
function setText(id, value) {
document.getElementById(id).textContent = String(value);
}
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 parseMethods(v) {
const items = String(v || '').split(',').map(s => s.trim().toUpperCase()).filter(Boolean);
return items.length ? Array.from(new Set(items)) : ['GET'];
}
function triggerByFn(fnName) {
return (S.triggers || []).find(function (t) {
return t.spec && t.spec.functionref && t.spec.functionref.name === fnName;
});
}
function h(v) {
return String(v == null ? '' : v)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
const LANG_TEMPLATES = {
python: {
entrypoint: 'main.main',
code: 'def main():\n return "hello from fission"'
},
nodejs: {
entrypoint: 'handler',
code: 'module.exports = async function(context) {\n return {\n status: 200,\n body: "hello from fission"\n };\n}'
},
go: {
entrypoint: 'Handler',
code: 'package main\n\nimport (\n "fmt"\n "net/http"\n)\n\nfunc Handler(w http.ResponseWriter, r *http.Request) {\n fmt.Fprintf(w, "hello from fission")\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'
},
perl: {
entrypoint: 'handler',
code: 'sub {\n return "hello from fission";\n}'
}
};
function onLangChange() {
var lang = document.getElementById('c-lang').value;
var t = LANG_TEMPLATES[lang];
if (t) {
document.getElementById('c-entry').value = t.entrypoint;
document.getElementById('c-code').value = t.code;
}
}
function openCreate() {
document.getElementById('c-lang').value = 'python';
onLangChange();
document.getElementById('create-modal').classList.add('open');
document.getElementById('c-name').focus();
}
function closeCreate() {
document.getElementById('create-modal').classList.remove('open');
}
async function submitCreate() {
const btn = document.getElementById('c-submit');
btn.disabled = true;
try {
const name = document.getElementById('c-name').value.trim();
if (!name) throw new Error('name is required');
const lang = document.getElementById('c-lang').value.trim();
if (!lang) throw new Error('language is required');
await requestJSON(API_BASE + '/functions', 'POST', {
name: name,
language: lang,
entrypoint: document.getElementById('c-entry').value.trim(),
route: document.getElementById('c-route').value.trim(),
methods: parseMethods(document.getElementById('c-methods').value),
code: document.getElementById('c-code').value
});
closeCreate();
showStatus('\u0424\u0443\u043d\u043a\u0446\u0438\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0430: ' + name, 'ok');
await reloadAll();
} catch (e) {
showStatus('\u041e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f: ' + 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 = '\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435: ' + name;
document.getElementById('e-name').value = name;
document.getElementById('e-env').value = fn.environment || '';
document.getElementById('e-entry').value = fn.entrypoint || '';
document.getElementById('e-code').value = fn.code || '';
// Определяем язык по имени environment для линтера
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';
else if (envName.includes('perl')) lang = 'perl';
document.getElementById('e-lang-hidden').value = lang;
// сбросить предыдущий AI-результат
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('\u041e\u0448\u0438\u0431\u043a\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0444\u0443\u043d\u043a\u0446\u0438\u0438: ' + 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;
try {
const name = S.currentEdit.name;
await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name) + '/code', 'PUT', {
code: document.getElementById('e-code').value
});
closeEdit();
showStatus('\u041a\u043e\u0434 \u043e\u0431\u043d\u043e\u0432\u043b\u0451\u043d: ' + name, 'ok');
} catch (e) {
showStatus('\u041e\u0448\u0438\u0431\u043a\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f: ' + e.message, 'err');
} finally {
btn.disabled = false;
}
}
function openInvoke(name) {
S.currentInvoke = name;
document.getElementById('i-title').textContent = '\u0412\u044b\u0437\u043e\u0432: ' + name;
document.getElementById('i-resp').value = '';
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 respEl = document.getElementById('i-resp');
btn.disabled = true;
respEl.value = '';
let elapsed = 0;
statusEl.textContent = 'Вызов...';
const timer = setInterval(() => {
elapsed++;
if (elapsed < 5) {
statusEl.textContent = 'Вызов... ' + elapsed + 'с';
} else if (elapsed < 10) {
statusEl.textContent = '⏳ Холодный старт — прогрев пула... ' + elapsed + 'с';
} else {
statusEl.textContent = '⏳ Холодный старт — ещё немного... ' + 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);
statusEl.textContent = '✓ Выполнено за ' + elapsed + 'с';
respEl.value = JSON.stringify(result, null, 2);
} catch (e) {
statusEl.textContent = '✗ Ошибка после ' + elapsed + 'с';
respEl.value = 'Ошибка вызова: ' + e.message;
} finally {
clearInterval(timer);
btn.disabled = false;
}
}
async function removeFn(name) {
var tfWarn = (/^tf-/.test(name)) ? '\n\n\u26a0\ufe0f \u042d\u0442\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f Terraform. \u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u0438\u0432\u0435\u0434\u0451\u0442 \u043a \u0440\u0430\u0441\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u0438 state!' : '';
if (!confirm('\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0444\u0443\u043d\u043a\u0446\u0438\u044e ' + name + '?' + tfWarn)) return;
try {
await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name), 'DELETE');
showStatus('\u0424\u0443\u043d\u043a\u0446\u0438\u044f \u0443\u0434\u0430\u043b\u0435\u043d\u0430: ' + name, 'ok');
await reloadAll();
} catch (e) {
showStatus('\u041e\u0448\u0438\u0431\u043a\u0430 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f: ' + e.message, 'err');
}
}
async function reloadAll() {
try {
const [envs, pkgs, fns, http, time] = 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.triggers = http || [];
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('time-count', time.length || 0);
const rows = (fns || []).map(function (f) {
const spec = f.spec || {};
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 = triggerByFn(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('');
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 class="mono">' + h(route) + '</td>' +
'<td>' + chips + '</td>' +
'<td class="nowrap">' + actions + '</td>' +
'</tr>';
}).join('');
document.getElementById('fn-rows').innerHTML = rows || '<tr><td colspan="3">\u041d\u0435\u0442 \u0444\u0443\u043d\u043a\u0446\u0438\u0439</td></tr>';
if (!rows) {
document.getElementById('fn-rows').innerHTML = '<tr><td colspan="6">\u041d\u0435\u0442 \u0444\u0443\u043d\u043a\u0446\u0438\u0439</td></tr>';
}
} catch (e) {
document.getElementById('fn-rows').innerHTML = '<tr><td colspan="6">Load error: ' + e.message + '</td></tr>';
showStatus('\u041e\u0448\u0438\u0431\u043a\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438: ' + e.message, 'err');
}
}
function showLoginOverlay() {
document.getElementById('login-overlay').style.display = 'flex';
}
function hideLoginOverlay() {
document.getElementById('login-overlay').style.display = 'none';
}
async function doLogin() {
var btn = document.getElementById('l-btn');
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; }
btn.disabled = true;
errEl.style.display = 'none';
try {
const res = await fetch(API_BASE + '/auth', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({token: token, env: env})
});
if (!res.ok) {
const d = await res.json().catch(function() { return {}; });
throw new Error(d.error || 'Ошибка входа');
}
localStorage.setItem('auth_token', token);
localStorage.setItem('auth_env', env);
hideLoginOverlay();
reloadAll();
} catch(e) {
errEl.textContent = e.message;
errEl.style.display = 'block';
} finally {
btn.disabled = false;
}
}
function doLogout() {
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_env');
try { document.getElementById('l-token').value = ''; } catch(_) {}
showLoginOverlay();
}
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 = 'Отправляю код в LLM...';
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 checkAuth() {
if (!localStorage.getItem('auth_token')) {
showLoginOverlay();
return;
}
hideLoginOverlay();
reloadAll();
}
checkAuth();
</script>
<!-- ================================================================
ai/ask feature — удалить весь блок до "end ai/ask feature"
================================================================ -->
<div id="assistant-panel" style="
position:fixed; bottom:0; right:24px;
width:720px; background:#1e1e2e; border:1px solid #3a3a5c;
border-bottom:none; border-radius:8px 8px 0 0;
font-family:inherit; z-index:900; box-shadow:0 -2px 12px rgba(0,0,0,.4);
">
<div id="assistant-header" onclick="toggleAssistant()" style="
display:flex; align-items:center; justify-content:space-between;
padding:8px 12px; cursor:pointer; background:#2a2a42; border-radius:8px 8px 0 0;
user-select:none;
">
<span style="font-weight:600; font-size:.85rem;">💬 Ассистент</span>
<span id="assistant-toggle-icon" style="font-size:.75rem; opacity:.7;"></span>
</div>
<div id="assistant-body" style="padding:10px; display:flex; flex-direction:column; gap:8px;">
<div id="assistant-messages" style="
min-height:80px; max-height:400px; overflow-y:auto;
font-size:.82rem; line-height:1.5; color:#cdd6f4;
background:#13131f; border-radius:4px; padding:8px;
white-space:pre-wrap; word-break:break-word;
">Чем могу помочь?</div>
<div style="display:flex; gap:6px;">
<input id="assistant-input" type="text" placeholder="Введи вопрос..."
style="flex:1; background:#2a2a42; border:1px solid #3a3a5c; border-radius:4px;
color:#cdd6f4; padding:6px 8px; font-size:.82rem; outline:none;"
onkeydown="if(event.key==='Enter')askAssistant()" />
<button onclick="askAssistant()" style="
background:#7c3aed; color:#fff; border:none; border-radius:4px;
padding:6px 12px; cursor:pointer; font-size:.82rem; white-space:nowrap;
"></button>
</div>
</div>
</div>
<script>
// ai/ask feature
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({ 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';
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 q = 'Напиши функцию Fission на ' + lang + ' с именем "' + name + '". Функция должна: ' + desc + '. Верни только чистый код без пояснений и без markdown-блоков.';
var d = await requestJSON('/console/api/ai/ask', 'POST', {question: q});
var code = (d.answer || '').replace(/^```[\w]*\n?/, '').replace(/\n?```$/, '');
document.getElementById('c-code').value = code;
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 = '✨ Сгенерировать код';
}
}
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 q = 'Кратко объясни что делает этот ' + lang + ' код:\n' + code;
var d = await requestJSON('/console/api/ai/ask', 'POST', {question: q});
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 = '📖 Что делает?';
}
}
// end ai/ask feature
</script>
<!-- end ai/ask feature -->
</body>
</html>