Files
sless/services/funcs/index.html
T
Naeel b7fa8acf76 feat(web-console): create/edit/delete functions in UI, v0.1.4
- services/funcs: новые маршруты POST create-function, POST save-function,
  DELETE delete-function — создание/редактирование/удаление через браузер
- index.html: модалка создания с шаблонами Python/Node.js hello world,
  кнопки edit/delete на каждой карточке функции
- sless-funcs-service:v0.1.4 задеплоен
- examples/POSTGRES: удалены логи, .bak, backup tfstate, лишние функции
  оставлены 2 Python (pg-stats, pg-counter) + 2 Node.js (pg-info, js-idempotent)
2026-03-23 10:19:42 +03:00

987 lines
36 KiB
HTML

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>sless console</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<style>
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
background: #0d1117;
color: #e6edf3;
min-height: 100vh;
}
a {
color: inherit;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #161b22;
}
::-webkit-scrollbar-thumb {
background: #30363d;
border-radius: 4px;
}
header {
padding: 14px 24px;
border-bottom: 1px solid #30363d;
display: flex;
align-items: center;
gap: 12px;
background: #161b22;
position: sticky;
top: 0;
z-index: 10;
}
.logo {
font-weight: 700;
font-size: 1.1rem;
color: #58a6ff;
letter-spacing: -0.3px;
}
.logo-slash {
color: #484f58;
margin: 0 4px;
}
.ns-badge {
background: #1c2128;
border: 1px solid #30363d;
padding: 3px 12px;
border-radius: 20px;
font-family: monospace;
font-size: 0.82rem;
color: #8b949e;
}
.refresh-btn {
margin-left: auto;
background: #21262d;
border: 1px solid #30363d;
color: #c9d1d9;
padding: 6px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.82rem;
transition: background 0.1s;
}
.refresh-btn:hover {
background: #30363d;
}
main {
padding: 24px;
max-width: 960px;
margin: 0 auto;
}
.summary {
color: #8b949e;
font-size: 0.82rem;
margin-bottom: 18px;
}
.fn-card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 8px;
margin-bottom: 10px;
overflow: hidden;
transition: border-color 0.15s;
}
.fn-card:hover {
border-color: #484f58;
}
.fn-header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
cursor: pointer;
user-select: none;
flex-wrap: wrap;
row-gap: 6px;
}
.fn-header:hover {
background: rgba(255, 255, 255, 0.03);
}
.fn-name {
font-weight: 600;
font-size: 0.92rem;
color: #e6edf3;
}
.badge {
display: inline-block;
font-size: 0.7rem;
padding: 2px 8px;
border-radius: 12px;
font-weight: 500;
white-space: nowrap;
flex-shrink: 0;
}
.badge-runtime {
background: #21262d;
border: 1px solid #30363d;
color: #8b949e;
}
.badge-Ready {
background: #1a4731;
border: 1px solid #1f6132;
color: #3fb950;
}
.badge-Building {
background: #382a0a;
border: 1px solid #5a4212;
color: #d29922;
}
.badge-Pending {
background: #21262d;
border: 1px solid #30363d;
color: #8b949e;
}
.badge-Failed {
background: #3d1a1a;
border: 1px solid #6b1a1a;
color: #f85149;
}
.badge-kind-service {
background: #0d2538;
border: 1px solid #1158a7;
color: #58a6ff;
}
.badge-kind-function {
background: #1a2538;
border: 1px solid #30363d;
color: #8b949e;
}
.fn-url {
font-size: 0.76rem;
color: #58a6ff;
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 280px;
flex-shrink: 1;
}
.fn-url:hover {
text-decoration: underline;
}
.fn-cron {
font-size: 0.76rem;
color: #8b949e;
font-family: monospace;
}
.fn-msg {
font-size: 0.73rem;
color: #f85149;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.spacer {
flex: 1 0 0;
min-width: 4px;
}
.trigger-btn {
flex-shrink: 0;
padding: 4px 12px;
border-radius: 6px;
border: 1px solid transparent;
cursor: pointer;
font-size: 0.76rem;
font-weight: 500;
line-height: 1.5;
transition: opacity 0.1s, background 0.1s;
}
.trigger-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.trigger-btn.state-on {
background: #21262d;
border-color: #1f6132;
color: #3fb950;
}
.trigger-btn.state-on:hover:not(:disabled) {
background: #1a4731;
}
.trigger-btn.state-off {
background: #21262d;
border-color: #6b1a1a;
color: #f85149;
}
.trigger-btn.state-off:hover:not(:disabled) {
background: #3d1a1a;
}
.expand-icon {
color: #8b949e;
flex-shrink: 0;
font-size: 0.65rem;
transition: transform 0.18s;
}
.fn-body {
border-top: 1px solid #30363d;
}
.src-msg {
padding: 14px 16px;
color: #8b949e;
font-size: 0.85rem;
font-style: italic;
}
.file-block {
border-bottom: 1px solid #21262d;
}
.file-block:last-child {
border-bottom: none;
}
.file-header {
padding: 6px 16px;
background: #0d1117;
font-family: monospace;
font-size: 0.76rem;
color: #8b949e;
border-bottom: 1px solid #21262d;
}
.file-block pre {
margin: 0;
}
.file-block pre code.hljs {
border-radius: 0 !important;
font-size: 0.78rem !important;
padding: 14px 16px !important;
line-height: 1.5 !important;
}
.empty-state {
text-align: center;
padding: 64px 24px;
color: #484f58;
font-size: 0.9rem;
}
/* Кнопки действий на карточке функции */
.action-btn {
flex-shrink: 0;
padding: 3px 10px;
border-radius: 5px;
border: 1px solid #30363d;
cursor: pointer;
font-size: 0.73rem;
font-weight: 500;
background: #21262d;
color: #8b949e;
transition: background 0.1s, color 0.1s;
}
.action-btn:hover { background: #30363d; color: #c9d1d9; }
.action-btn.del:hover { background: #3d1a1a; border-color: #6b1a1a; color: #f85149; }
/* Кодовый редактор во встроенном режиме */
.edit-area {
width: 100%;
min-height: 220px;
background: #0d1117;
color: #e6edf3;
border: none;
border-top: 1px solid #30363d;
padding: 14px 16px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 0.78rem;
line-height: 1.55;
resize: vertical;
outline: none;
display: block;
box-sizing: border-box;
}
.edit-actions {
display: flex;
gap: 8px;
padding: 8px 14px;
background: #161b22;
border-top: 1px solid #30363d;
}
.edit-save-btn {
padding: 5px 16px;
background: #238636;
border: 1px solid #2ea043;
color: #fff;
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
}
.edit-save-btn:hover { background: #2ea043; }
.edit-save-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.edit-cancel-btn {
padding: 5px 16px;
background: #21262d;
border: 1px solid #30363d;
color: #c9d1d9;
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
}
.edit-status {
font-size: 0.77rem;
padding: 5px 8px;
align-self: center;
color: #8b949e;
}
.edit-status.ok { color: #3fb950; }
.edit-status.err { color: #f85149; }
/* ---------- Модалка создания функции ---------- */
.modal-backdrop {
display: none;
position: fixed; inset: 0;
background: rgba(0,0,0,0.65);
z-index: 100;
align-items: center;
justify-content: center;
}
.modal-backdrop.open { display: flex; }
.modal {
background: #161b22;
border: 1px solid #30363d;
border-radius: 10px;
width: min(660px, 95vw);
max-height: 90vh;
overflow-y: auto;
padding: 24px;
display: flex;
flex-direction: column;
gap: 14px;
}
.modal h2 {
font-size: 1rem;
font-weight: 600;
color: #e6edf3;
}
.modal label {
font-size: 0.82rem;
color: #8b949e;
display: block;
margin-bottom: 4px;
}
.modal input, .modal select {
width: 100%;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
color: #e6edf3;
padding: 7px 10px;
font-size: 0.85rem;
box-sizing: border-box;
outline: none;
}
.modal input:focus, .modal select:focus { border-color: #58a6ff; }
.modal-row { display: flex; gap: 12px; }
.modal-row > div { flex: 1; }
.modal-code {
width: 100%;
min-height: 200px;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 6px;
color: #e6edf3;
padding: 10px 12px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
font-size: 0.78rem;
line-height: 1.55;
resize: vertical;
outline: none;
box-sizing: border-box;
}
.modal-code:focus { border-color: #58a6ff; }
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.modal-create-btn {
padding: 7px 20px;
background: #238636;
border: 1px solid #2ea043;
color: #fff;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
}
.modal-create-btn:hover { background: #2ea043; }
.modal-create-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.modal-cancel-btn {
padding: 7px 20px;
background: #21262d;
border: 1px solid #30363d;
color: #c9d1d9;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
}
.modal-err {
color: #f85149;
font-size: 0.8rem;
min-height: 18px;
}
.create-fn-btn {
margin-left: auto;
background: #238636;
border: 1px solid #2ea043;
color: #fff;
padding: 6px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.82rem;
transition: background 0.1s;
}
.create-fn-btn:hover { background: #2ea043; }
</style>
</head>
<body>
<header>
<span class="logo">sless</span>
<span class="logo-slash">/</span>
<span class="ns-badge" id="ns-label"></span>
<button class="refresh-btn" onclick="location.reload()">↻ обновить</button>
<button class="create-fn-btn" id="open-create-modal">+ Создать функцию</button>
</header>
<main>
<p class="summary" id="summary"></p>
<div id="fn-list"></div>
</main>
<!-- Модалка создания новой функции -->
<div class="modal-backdrop" id="create-modal">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<h2 id="modal-title">Создать функцию</h2>
<div>
<label>Runtime</label>
<select id="m-runtime">
<option value="python3.11">Python 3.11</option>
<option value="nodejs20">Node.js 20</option>
</select>
</div>
<div class="modal-row">
<div>
<label>Имя функции (k8s name)</label>
<input id="m-name" type="text" placeholder="my-hello-fn" pattern="[a-z0-9][a-z0-9\-]{0,61}[a-z0-9]">
</div>
<div>
<label>Entrypoint (module.func)</label>
<input id="m-entrypoint" type="text" value="handler.handler">
</div>
</div>
<div class="modal-row">
<div>
<label>Память (MB)</label>
<input id="m-memory" type="number" value="128" min="64" max="4096">
</div>
<div>
<label>Таймаут (сек)</label>
<input id="m-timeout" type="number" value="30" min="1" max="900">
</div>
</div>
<div>
<label id="m-filename-label">Файл: <code id="m-filename-display">handler.py</code></label>
<textarea class="modal-code" id="m-code" spellcheck="false"></textarea>
</div>
<div class="modal-err" id="m-err"></div>
<div class="modal-actions">
<button class="modal-cancel-btn" id="modal-cancel">Отмена</button>
<button class="modal-create-btn" id="modal-submit">Создать</button>
</div>
</div>
</div>
<script id="fndata" type="application/json">__PAGE_DATA__</script>
<script>
(function () {
'use strict';
var raw = document.getElementById('fndata').textContent;
var data = JSON.parse(raw);
var ns = data.namespace;
var extURL = data.externalURL || '';
var fns = data.functions || [];
var byFn = data.triggersByFn || {};
document.getElementById('ns-label').textContent = ns;
var svcsCount = fns.filter(function(f){ return f.kind === 'service'; }).length;
var fnsCount = fns.filter(function(f){ return f.kind !== 'service'; }).length;
document.getElementById('summary').textContent =
'Функций: ' + fnsCount + ' | Сервисов: ' + svcsCount + ' | Всего: ' + fns.length;
var list = document.getElementById('fn-list');
if (fns.length === 0) {
var empty = $el('div', 'empty-state');
empty.textContent = 'Нет функций в этом namespace';
list.appendChild(empty);
}
for (var i = 0; i < fns.length; i++) {
list.appendChild(buildCard(fns[i], byFn[fns[i].name] || []));
}
// ---------- Карточки функций ----------
function buildCard(fn, triggers) {
var card = $el('div', 'fn-card');
var hdr = $el('div', 'fn-header');
var body = $el('div', 'fn-body');
body.style.display = 'none';
$txt(hdr, 'span', 'fn-name', fn.name);
$txt(hdr, 'span', 'badge badge-runtime', fn.runtime);
$txt(hdr, 'span', 'badge badge-' + fn.phase, fn.phase || 'Pending');
if (fn.kind) {
var kindLabel = fn.kind === 'service' ? 'always-on' : 'job';
$txt(hdr, 'span', 'badge badge-kind-' + fn.kind, kindLabel);
}
if (fn.url) {
var a = document.createElement('a');
a.className = 'fn-url';
a.href = fn.url;
a.target = '_blank';
a.rel = 'noopener noreferrer';
a.textContent = fn.url;
a.addEventListener('click', function (e) { e.stopPropagation(); });
hdr.appendChild(a);
}
for (var t = 0; t < triggers.length; t++) {
var tr = triggers[t];
if (tr.type === 'http') {
if (!fn.url) {
var url = extURL ? extURL + '/fn/' + ns + '/' + fn.name : (tr.url || '');
if (url) {
var a = document.createElement('a');
a.className = 'fn-url';
a.href = url;
a.target = '_blank';
a.rel = 'noopener noreferrer';
a.textContent = url;
a.addEventListener('click', function (e) { e.stopPropagation(); });
hdr.appendChild(a);
}
}
hdr.appendChild(makeTriggerBtn(tr, ns));
} else if (tr.type === 'cron') {
$txt(hdr, 'span', 'fn-cron', 'cron: ' + tr.schedule);
hdr.appendChild(makeTriggerBtn(tr, ns));
}
}
if (fn.message) $txt(hdr, 'span', 'fn-msg', fn.message);
hdr.appendChild($el('span', 'spacer'));
// Кнопка редактирования кода
var editBtn = $el('button', 'action-btn');
editBtn.textContent = '✎';
editBtn.title = 'Редактировать код';
editBtn.addEventListener('click', function (e) {
e.stopPropagation();
body.style.display = 'block';
icon.style.transform = 'rotate(90deg)';
openEditMode(body, ns, fn.name, fn.kind, fn.runtime);
});
hdr.appendChild(editBtn);
// Кнопка удаления
var delBtn = $el('button', 'action-btn del');
delBtn.textContent = '✕';
delBtn.title = 'Удалить функцию';
delBtn.addEventListener('click', function (e) {
e.stopPropagation();
if (!confirm('Удалить функцию «' + fn.name + '»?\n\nЭто действие необратимо.')) return;
delBtn.disabled = true;
fetch('/funcs/' + ns + '/delete-function/' + fn.name + '?kind=' + (fn.kind || 'function'), {
method: 'DELETE'
}).then(function (r) {
if (r.ok) {
card.remove();
} else {
return r.text().then(function (t) { alert('Ошибка удаления: ' + t); });
}
}).catch(function (err) {
alert('Сетевая ошибка: ' + err);
}).finally(function () {
delBtn.disabled = false;
});
});
hdr.appendChild(delBtn);
var icon = $txt(hdr, 'span', 'expand-icon', '▶');
var loaded = false;
hdr.addEventListener('click', function () {
if (body.style.display === 'none') {
body.style.display = 'block';
icon.style.transform = 'rotate(90deg)';
if (!loaded) {
loaded = true;
loadSource(body, ns, fn.name, fn.kind);
}
} else {
body.style.display = 'none';
icon.style.transform = '';
}
});
card.appendChild(hdr);
card.appendChild(body);
return card;
}
// openEditMode — открывает тело карточки в режиме редактирования.
// Загружает текущий код, показывает textarea, кнопки Сохранить/Закрыть.
function openEditMode(body, ns, fnName, fnKind, fnRuntime) {
body.innerHTML = '';
var msg = $el('div', 'src-msg');
msg.textContent = 'Загрузка кода…';
body.appendChild(msg);
var kindParam = fnKind === 'service' ? '?kind=service' : '';
fetch('/funcs/' + ns + '/source/' + fnName + kindParam)
.then(function (r) {
body.removeChild(msg);
if (!r.ok) {
var e = $el('div', 'src-msg');
e.textContent = 'Ошибка загрузки кода: ' + r.status;
body.appendChild(e);
return;
}
return r.json().then(function (files) {
var editable = files.filter(function (f) {
return !f.binary && (f.name.endsWith('.py') || f.name.endsWith('.js'));
});
if (editable.length === 0) {
// Нет кода — показываем пустой редактор с шаблоном
editable = [{ name: defaultFilename(fnRuntime), content: helloWorldTemplate(fnRuntime) }];
}
var f = editable[0];
renderEditArea(body, ns, fnName, fnKind, f.name, f.content);
});
})
.catch(function (err) {
body.innerHTML = '';
var e = $el('div', 'src-msg');
e.textContent = 'Ошибка: ' + err;
body.appendChild(e);
});
}
// renderEditArea — рисует textarea + кнопки Save/Cancel в теле карточки.
function renderEditArea(body, ns, fnName, fnKind, filename, initialCode) {
body.innerHTML = '';
var ta = document.createElement('textarea');
ta.className = 'edit-area';
ta.spellcheck = false;
ta.value = initialCode;
body.appendChild(ta);
var actions = $el('div', 'edit-actions');
var saveBtn = $el('button', 'edit-save-btn');
saveBtn.textContent = 'Сохранить';
var cancelBtn = $el('button', 'edit-cancel-btn');
cancelBtn.textContent = 'Закрыть';
var status = $el('span', 'edit-status');
actions.appendChild(saveBtn);
actions.appendChild(cancelBtn);
actions.appendChild(status);
body.appendChild(actions);
saveBtn.addEventListener('click', function () {
saveBtn.disabled = true;
status.textContent = 'Сохраняю…';
status.className = 'edit-status';
fetch('/funcs/' + ns + '/save-function/' + fnName, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: ta.value, filename: filename, kind: fnKind || 'function' })
}).then(function (r) {
if (r.ok) {
status.textContent = '✓ Сохранено — сборка запущена';
status.className = 'edit-status ok';
} else {
return r.text().then(function (t) {
status.textContent = '✗ ' + t;
status.className = 'edit-status err';
});
}
}).catch(function (err) {
status.textContent = '✗ ' + err;
status.className = 'edit-status err';
}).finally(function () {
saveBtn.disabled = false;
});
});
cancelBtn.addEventListener('click', function () {
body.style.display = 'none';
});
}
function makeTriggerBtn(tr, ns) {
var btn = $el('button', 'trigger-btn ' + (tr.enabled ? 'state-on' : 'state-off'));
btn.textContent = tr.enabled ? '■ стоп' : '▶ запуск';
btn.title = tr.name;
btn.addEventListener('click', function (e) {
e.stopPropagation();
btn.disabled = true;
var newVal = !tr.enabled;
fetch('/funcs/' + ns + '/triggers/' + tr.name, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: newVal })
}).then(function (r) {
if (r.ok) {
tr.enabled = newVal;
btn.className = 'trigger-btn ' + (newVal ? 'state-on' : 'state-off');
btn.textContent = newVal ? '■ стоп' : '▶ запуск';
} else {
return r.text().then(function (t) { alert('Ошибка: ' + t); });
}
}).catch(function (err) {
alert('Сетевая ошибка: ' + err);
}).finally(function () {
btn.disabled = false;
});
});
return btn;
}
function loadSource(body, ns, fnName, fnKind) {
var msg = $el('div', 'src-msg');
msg.textContent = 'Загрузка кода…';
body.appendChild(msg);
var kindParam = fnKind === 'service' ? '?kind=service' : '';
fetch('/funcs/' + ns + '/source/' + fnName + kindParam)
.then(function (r) {
body.removeChild(msg);
if (!r.ok) {
var e = $el('div', 'src-msg');
e.textContent = 'Ошибка ' + r.status;
body.appendChild(e);
return;
}
return r.json().then(function (files) {
var shown = files.filter(function (f) { return !f.binary; });
if (shown.length === 0) {
var e = $el('div', 'src-msg');
e.textContent = 'Код не загружен';
body.appendChild(e);
return;
}
for (var i = 0; i < shown.length; i++) {
var f = shown[i];
var block = $el('div', 'file-block');
var fhdr = $el('div', 'file-header');
fhdr.textContent = f.name;
var pre = document.createElement('pre');
var code = document.createElement('code');
code.className = 'language-' + detectLang(f.name);
code.textContent = f.content;
pre.appendChild(code);
block.appendChild(fhdr);
block.appendChild(pre);
body.appendChild(block);
}
hljs.highlightAll();
});
})
.catch(function (err) {
body.innerHTML = '';
var e = $el('div', 'src-msg');
e.textContent = 'Ошибка: ' + err;
body.appendChild(e);
});
}
// ---------- Модалка создания функции ----------
var TEMPLATES = {
'python3.11': {
filename: 'handler.py',
code: '# Hello World — Python 3.11\n# Entrypoint: handler.handler\n\ndef handler(event, context):\n name = event.get("name", "World")\n return {"result": f"Hello, {name}!"}\n'
},
'nodejs20': {
filename: 'handler.js',
code: '// Hello World — Node.js 20\n// Entrypoint: handler.handler\n\nexports.handler = async (event, context) => {\n const name = event.name || \'World\';\n return { result: `Hello, ${name}!` };\n};\n'
}
};
function defaultFilename(runtime) {
return TEMPLATES[runtime] ? TEMPLATES[runtime].filename : 'handler.py';
}
function helloWorldTemplate(runtime) {
return TEMPLATES[runtime] ? TEMPLATES[runtime].code : '# код функции\n';
}
var modal = document.getElementById('create-modal');
var mRuntime = document.getElementById('m-runtime');
var mCode = document.getElementById('m-code');
var mFilenameDisplay = document.getElementById('m-filename-display');
var mErr = document.getElementById('m-err');
// Заполняем шаблон при открытии / смене runtime
function applyTemplate() {
var tpl = TEMPLATES[mRuntime.value];
if (tpl) {
mCode.value = tpl.code;
mFilenameDisplay.textContent = tpl.filename;
}
}
applyTemplate();
mRuntime.addEventListener('change', applyTemplate);
document.getElementById('open-create-modal').addEventListener('click', function () {
applyTemplate();
mErr.textContent = '';
document.getElementById('m-name').value = '';
document.getElementById('m-entrypoint').value = 'handler.handler';
document.getElementById('m-memory').value = '128';
document.getElementById('m-timeout').value = '30';
modal.classList.add('open');
document.getElementById('m-name').focus();
});
document.getElementById('modal-cancel').addEventListener('click', function () {
modal.classList.remove('open');
});
modal.addEventListener('click', function (e) {
if (e.target === modal) modal.classList.remove('open');
});
document.getElementById('modal-submit').addEventListener('click', function () {
var name = document.getElementById('m-name').value.trim();
var runtime = mRuntime.value;
var entrypoint = document.getElementById('m-entrypoint').value.trim();
var memory = parseInt(document.getElementById('m-memory').value, 10);
var timeout = parseInt(document.getElementById('m-timeout').value, 10);
var code = mCode.value;
var filename = TEMPLATES[runtime] ? TEMPLATES[runtime].filename : 'handler.py';
mErr.textContent = '';
if (!name) { mErr.textContent = 'Укажите имя функции'; return; }
if (!/^[a-z0-9][a-z0-9\-]{0,61}[a-z0-9]$/.test(name) && !/^[a-z0-9]$/.test(name)) {
mErr.textContent = 'Имя должно быть в формате k8s: строчные буквы, цифры, дефисы';
return;
}
if (!entrypoint) { mErr.textContent = 'Укажите entrypoint (например handler.handler)'; return; }
if (!code.trim()) { mErr.textContent = 'Напишите код функции'; return; }
var btn = document.getElementById('modal-submit');
btn.disabled = true;
mErr.textContent = 'Создаю…';
fetch('/funcs/' + ns + '/create-function', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: name,
runtime: runtime,
code: code,
filename: filename,
entrypoint: entrypoint,
memory_mb: memory,
timeout_sec: timeout
})
}).then(function (r) {
if (r.ok) {
modal.classList.remove('open');
mErr.textContent = '';
// Перезагружаем страницу чтобы увидеть новую функцию
setTimeout(function () { location.reload(); }, 400);
} else {
return r.text().then(function (t) {
mErr.textContent = '✗ ' + t;
});
}
}).catch(function (err) {
mErr.textContent = '✗ Сетевая ошибка: ' + err;
}).finally(function () {
btn.disabled = false;
});
});
// ---------- Утилиты ----------
function detectLang(name) {
var ext = name.split('.').pop().toLowerCase();
var m = {
py: 'python', js: 'javascript', ts: 'typescript', go: 'go',
sh: 'bash', bash: 'bash', yml: 'yaml', yaml: 'yaml',
json: 'json', md: 'markdown', txt: 'plaintext', toml: 'toml',
rs: 'rust', rb: 'ruby', java: 'java', cpp: 'cpp', c: 'c',
cs: 'csharp', php: 'php', html: 'html', css: 'css'
};
return m[ext] || 'plaintext';
}
function $el(tag, cls) { var e = document.createElement(tag); if (cls) e.className = cls; return e; }
function $txt(p, tag, cls, text) { var e = $el(tag, cls); e.textContent = text; p.appendChild(e); return e; }
})();
</script>
</body>
</html>