diff --git a/console/deploy/console.yaml b/console/deploy/console.yaml
index 883701f..4129237 100644
--- a/console/deploy/console.yaml
+++ b/console/deploy/console.yaml
@@ -52,7 +52,7 @@ spec:
serviceAccountName: fission-console
containers:
- name: console
- image: naeel/fission-console:v1.3.45
+ image: naeel/fission-console:v1.3.46
imagePullPolicy: Always
ports:
- containerPort: 8090
diff --git a/console/ui/index.html b/console/ui/index.html
index c804f51..cfcca53 100644
--- a/console/ui/index.html
+++ b/console/ui/index.html
@@ -21,6 +21,8 @@
+
+
@@ -100,11 +102,12 @@
+
+
-
Создать функцию
+
✏️ Создать функцию из кода
-
-
-
-
-
+
+
+
+
+
+
📦 Создать функцию из архива
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -400,7 +460,7 @@
- v1.3.45
+ v1.3.46
diff --git a/console/ui/js/ai.js b/console/ui/js/ai.js
index 56f8719..846eaf8 100644
--- a/console/ui/js/ai.js
+++ b/console/ui/js/ai.js
@@ -86,19 +86,21 @@ async function askAssistant() {
msgs.scrollTop = msgs.scrollHeight;
}
-function showGenPrompt() {
- var el = document.getElementById('c-gen-prompt');
+function showGenPrompt(prefix) {
+ prefix = prefix || 'cc';
+ var el = document.getElementById(prefix + '-gen-prompt');
el.style.display = 'flex';
- document.getElementById('c-gen-desc').focus();
+ document.getElementById(prefix + '-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');
+async function aiGenerate(prefix) {
+ prefix = prefix || 'cc';
+ var name = (document.getElementById(prefix + '-name').value || '').trim() || 'my-func';
+ var lang = document.getElementById(prefix + '-lang').value || 'python';
+ var desc = (document.getElementById(prefix + '-gen-desc').value || '').trim();
+ if (!desc) { document.getElementById(prefix + '-gen-desc').focus(); return; }
+ var btn = document.getElementById(prefix + '-gen-btn');
+ var resEl = document.getElementById(prefix + '-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...';
@@ -110,8 +112,8 @@ async function aiGenerate() {
description: desc
});
var code = (d.answer || '').replace(/^```[\w]*\n?/, '').replace(/\n?```$/, '');
- document.getElementById('c-code').value = addLLMWarning(code, lang);
- document.getElementById('c-gen-prompt').style.display = 'none';
+ document.getElementById(prefix + '-code').value = addLLMWarning(code, lang);
+ document.getElementById(prefix + '-gen-prompt').style.display = 'none';
resEl.style.background = '#1a3a1a'; resEl.style.color = '#8f8';
resEl.textContent = 'Код сгенерирован и вставлен.';
} catch (e) {
diff --git a/console/ui/js/fn-archive.js b/console/ui/js/fn-archive.js
new file mode 100644
index 0000000..2a41020
--- /dev/null
+++ b/console/ui/js/fn-archive.js
@@ -0,0 +1,83 @@
+/* fn-archive.js — создание функции из архива (независимый модуль) */
+
+function onLangChangeArchive() {
+ var lang = document.getElementById('ca-lang').value;
+ var t = LANG_TEMPLATES[lang];
+ if (t) {
+ document.getElementById('ca-entry').value = t.entrypoint;
+ }
+}
+
+function openCreateArchive() {
+ document.getElementById('ca-lang').value = 'python';
+ onLangChangeArchive();
+ document.getElementById('ca-name').value = '';
+ document.getElementById('ca-route').value = '';
+ document.getElementById('ca-methods').value = 'GET';
+ document.getElementById('ca-timeout').value = '60';
+ document.getElementById('ca-schedule-enabled').checked = false;
+ document.getElementById('ca-cron').value = '';
+ toggleScheduleFields('ca');
+ var fileInput = document.getElementById('ca-archive-file');
+ if (fileInput) fileInput.value = '';
+ var aiRes = document.getElementById('ca-ai-result');
+ if (aiRes) { aiRes.style.display = 'none'; aiRes.textContent = ''; }
+ document.getElementById('create-archive-modal').classList.add('open');
+ document.getElementById('ca-name').focus();
+}
+
+function closeCreateArchive() {
+ document.getElementById('create-archive-modal').classList.remove('open');
+}
+
+async function submitCreateArchive() {
+ const btn = document.getElementById('ca-submit');
+ btn.disabled = true;
+ const progress = startTimedStatus('Создаём функцию...', 'Создание функции...', explainDelay);
+ try {
+ const name = document.getElementById('ca-name').value.trim();
+ if (!name) throw new Error('name is required');
+ const lang = document.getElementById('ca-lang').value.trim();
+ if (!lang) throw new Error('language is required');
+ const archiveFile = document.getElementById('ca-archive-file').files[0];
+ if (!archiveFile) throw new Error('выберите .zip архив');
+
+ var fd = new FormData();
+ fd.append('name', name);
+ fd.append('language', lang);
+ fd.append('entrypoint', document.getElementById('ca-entry').value.trim());
+ fd.append('route', document.getElementById('ca-route').value.trim());
+ fd.append('methods', document.getElementById('ca-methods').value.trim());
+ fd.append('timeout', String(parseTimeout(document.getElementById('ca-timeout').value)));
+ fd.append('archive', archiveFile);
+
+ var resp = await fetch(API_BASE + '/functions', {
+ method: 'POST',
+ headers: authHeaders(),
+ body: fd
+ });
+ if (!resp.ok) {
+ var e = await resp.json().catch(() => ({}));
+ throw new Error(e.error || resp.statusText);
+ }
+
+ try {
+ await syncScheduleForFunction(name, 'ca');
+ } catch (scheduleErr) {
+ try {
+ await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name), 'DELETE');
+ } catch (rollbackErr) {
+ scheduleErr.message += '; rollback failed: ' + rollbackErr.message;
+ }
+ throw scheduleErr;
+ }
+
+ closeCreateArchive();
+ progress.stop('Функция создана: ' + name, 'ok');
+ await reloadAll();
+ } catch (e) {
+ progress.stop('Ошибка создания: ' + e.message, 'err');
+ } finally {
+ btn.disabled = false;
+ }
+}
diff --git a/console/ui/js/fn-code.js b/console/ui/js/fn-code.js
new file mode 100644
index 0000000..61d8748
--- /dev/null
+++ b/console/ui/js/fn-code.js
@@ -0,0 +1,71 @@
+/* fn-code.js — создание функции из кода (независимый модуль) */
+
+function onLangChangeCode() {
+ var lang = document.getElementById('cc-lang').value;
+ var t = LANG_TEMPLATES[lang];
+ if (t) {
+ document.getElementById('cc-entry').value = t.entrypoint;
+ document.getElementById('cc-code').value = t.code;
+ }
+}
+
+function openCreateCode() {
+ document.getElementById('cc-lang').value = 'python';
+ onLangChangeCode();
+ document.getElementById('cc-name').value = '';
+ document.getElementById('cc-route').value = '';
+ document.getElementById('cc-methods').value = 'GET';
+ document.getElementById('cc-timeout').value = '60';
+ document.getElementById('cc-schedule-enabled').checked = false;
+ document.getElementById('cc-cron').value = '';
+ toggleScheduleFields('cc');
+ var aiRes = document.getElementById('cc-ai-result');
+ if (aiRes) { aiRes.style.display = 'none'; aiRes.textContent = ''; }
+ document.getElementById('create-code-modal').classList.add('open');
+ document.getElementById('cc-name').focus();
+}
+
+function closeCreateCode() {
+ document.getElementById('create-code-modal').classList.remove('open');
+}
+
+async function submitCreateCode() {
+ const btn = document.getElementById('cc-submit');
+ btn.disabled = true;
+ const progress = startTimedStatus('Создаём функцию...', 'Создание функции...', explainDelay);
+ try {
+ const name = document.getElementById('cc-name').value.trim();
+ if (!name) throw new Error('name is required');
+ const lang = document.getElementById('cc-lang').value.trim();
+ if (!lang) throw new Error('language is required');
+
+ await requestJSON(API_BASE + '/functions', 'POST', {
+ name: name,
+ language: lang,
+ entrypoint: document.getElementById('cc-entry').value.trim(),
+ route: document.getElementById('cc-route').value.trim(),
+ methods: parseMethods(document.getElementById('cc-methods').value),
+ timeout: parseTimeout(document.getElementById('cc-timeout').value),
+ code: document.getElementById('cc-code').value
+ });
+
+ try {
+ await syncScheduleForFunction(name, 'cc');
+ } catch (scheduleErr) {
+ try {
+ await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name), 'DELETE');
+ } catch (rollbackErr) {
+ scheduleErr.message += '; rollback failed: ' + rollbackErr.message;
+ }
+ throw scheduleErr;
+ }
+
+ closeCreateCode();
+ progress.stop('Функция создана: ' + name, 'ok');
+ await reloadAll();
+ } catch (e) {
+ progress.stop('Ошибка создания: ' + e.message, 'err');
+ } finally {
+ btn.disabled = false;
+ }
+}
diff --git a/console/ui/js/functions.js b/console/ui/js/functions.js
index 85fd5bb..10b0942 100644
--- a/console/ui/js/functions.js
+++ b/console/ui/js/functions.js
@@ -133,89 +133,7 @@ function parseTimeout(v) {
return Math.round(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('c-timeout').value = '60';
- document.getElementById('c-schedule-enabled').checked = false;
- document.getElementById('c-cron').value = '';
- toggleScheduleFields('c');
- 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;
- const progress = startTimedStatus('Создаём функцию...', 'Создание функции...', explainDelay);
- 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');
-
- // Определяем режим: archiveArea видна → archive mode
- var archiveArea = document.getElementById('c-archive-area');
- var isArchiveMode = archiveArea && archiveArea.style.display !== 'none';
- var archiveFile = isArchiveMode ? document.getElementById('c-archive-file').files[0] : null;
-
- if (isArchiveMode && archiveFile) {
- // Отправляем multipart/form-data с архивом
- var fd = new FormData();
- fd.append('name', name);
- fd.append('language', lang);
- fd.append('entrypoint', document.getElementById('c-entry').value.trim());
- fd.append('route', document.getElementById('c-route').value.trim());
- fd.append('methods', document.getElementById('c-methods').value.trim());
- fd.append('timeout', String(parseTimeout(document.getElementById('c-timeout').value)));
- fd.append('archive', archiveFile);
- var resp = await fetch(API_BASE + '/functions', { method: 'POST', headers: authHeaders(), body: fd });
- if (!resp.ok) { var e = await resp.json().catch(() => ({})); throw new Error(e.error || resp.statusText); }
- } else {
- 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),
- timeout: parseTimeout(document.getElementById('c-timeout').value),
- code: document.getElementById('c-code').value
- });
- }
-
- try {
- await syncScheduleForFunction(name, 'c');
- } catch (scheduleErr) {
- try {
- await requestJSON(API_BASE + '/functions/' + encodeURIComponent(name), 'DELETE');
- } catch (rollbackErr) {
- scheduleErr.message += '; rollback failed: ' + rollbackErr.message;
- }
- throw scheduleErr;
- }
-
- closeCreate();
- progress.stop('Функция создана: ' + name, 'ok');
- await reloadAll();
- } catch (e) {
- progress.stop('Ошибка создания: ' + e.message, 'err');
- } finally {
- btn.disabled = false;
- }
-}
+// openCreate/closeCreate/submitCreate перенесены в fn-code.js и fn-archive.js
async function openEdit(name) {
try {
diff --git a/doc/plans/separate-create-modals.md b/doc/plans/separate-create-modals.md
new file mode 100644
index 0000000..1f837f1
--- /dev/null
+++ b/doc/plans/separate-create-modals.md
@@ -0,0 +1,75 @@
+# План: разделение Create на два независимых модуля
+Ветка: `separate-create-modals`
+Дата: 2026-05-04
+
+---
+
+## Цель
+Разделить одну кнопку «Создать функцию» с переключателем код/архив на два полностью независимых модуля.
+Никакого общего состояния, никакого общего if/else.
+
+---
+
+## Задачи
+
+### 1. Создать `console/ui/js/fn-code.js` [ ]
+Содержит только:
+- `onLangChangeCode()` — читает `cc-lang`, пишет в `cc-entry`, `cc-code`
+- `openCreateCode()` — инит формы, открывает `create-code-modal`
+- `closeCreateCode()` — закрывает `create-code-modal`
+- `submitCreateCode()` — отправляет JSON `POST /functions`, без архивной ветки
+
+### 2. Создать `console/ui/js/fn-archive.js` [ ]
+Содержит только:
+- `onLangChangeArchive()` — читает `ca-lang`, пишет в `ca-entry`
+- `openCreateArchive()` — инит формы, открывает `create-archive-modal`
+- `closeCreateArchive()` — закрывает `create-archive-modal`
+- `submitCreateArchive()` — отправляет multipart `POST /functions`, без кодовой ветки
+
+### 3. Изменить `index.html` [ ]
+- Заменить 1 кнопку «+ Создать функцию» на две:
+ - `+ Из кода` → `openCreateCode()`
+ - `+ Из архива` → `openCreateArchive()`
+- Добавить `create-code-modal` (prefix `cc-`) — только textarea + AI-кнопки
+- Добавить `create-archive-modal` (prefix `ca-`) — только file input + lint/explain
+- Удалить старую `create-modal` с переключателями
+- Подключить `