-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/console/ui/js/functions.js b/console/ui/js/functions.js
index cc55407..e7d4d04 100644
--- a/console/ui/js/functions.js
+++ b/console/ui/js/functions.js
@@ -1,5 +1,33 @@
/* functions.js — CRUD операции с функциями */
+// setCodeMode переключает режим формы между вводом кода и загрузкой архива.
+// prefix: 'c' (create) или 'e' (edit)
+// mode: 'code' | 'archive'
+function setCodeMode(prefix, mode) {
+ var codeArea = document.getElementById(prefix + '-code-area');
+ var archiveArea = document.getElementById(prefix + '-archive-area');
+ var btnCode = document.getElementById(prefix + '-mode-code');
+ var btnArchive = document.getElementById(prefix + '-mode-archive');
+ if (!codeArea || !archiveArea) return;
+
+ var isCode = mode === 'code';
+ codeArea.style.display = isCode ? '' : 'none';
+ archiveArea.style.display = isCode ? 'none' : '';
+ if (btnCode) btnCode.style.opacity = isCode ? '1' : '0.5';
+ if (btnArchive) btnArchive.style.opacity = isCode ? '0.5' : '1';
+}
+
+// lintArchiveFile — заглушка линтера для архивов.
+// Реализация (распаковка + пофайловая проверка) добавляется позже.
+function lintArchiveFile(prefix) {
+ var input = document.getElementById(prefix + '-archive-file');
+ if (!input || !input.files || !input.files[0]) {
+ showStatus('Выберите .zip файл', 'err');
+ return;
+ }
+ showStatus('Линтинг архива — пока не реализован (скоро)', 'info');
+}
+
function parseMethods(v) {
const items = String(v || '').split(',').map(s => s.trim().toUpperCase()).filter(Boolean);
return items.length ? Array.from(new Set(items)) : ['GET'];
diff --git a/doc/descriptions/auth-flow.md b/doc/descriptions/auth-flow.md
index ea2bc24..c6661ff 100644
--- a/doc/descriptions/auth-flow.md
+++ b/doc/descriptions/auth-flow.md
@@ -1,5 +1,167 @@
# Логика авторизации — Fission Console
+> ⚠️ ЛЕГАСИ (актуально до ~апреля 2026): описание ниже относится к старой схеме с Deck API + JWT.
+> Актуальное описание — **выше этого блока**, в разделе "АКТУАЛЬНО (май 2026+)".
+
+---
+
+# АКТУАЛЬНО (май 2026+)
+
+## Что принимает сервер
+
+Токен передаётся одним из двух способов:
+- Заголовок `X-Auth-Token: <токен>`
+- Заголовок `Authorization: Bearer <токен>`
+
+`X-Auth-Env` — необязателен, по умолчанию `test`.
+
+---
+
+## Типы токенов и маршрутизация (MultiAuthenticator)
+
+Сервер использует `MultiAuthenticator`, который определяет тип по форме токена:
+
+| Форма токена | Authenticator | Описание |
+|---|---|---|
+| JWT (три части через `.`) | `DeckAuthenticator` или `TestAuthenticator` | Реальный облачный токен или тест-JWT |
+| Любая строка ≥6 символов | `DemoAuthenticator` | Demo-логин без внешних запросов |
+| Строка <6 символов | — | Ошибка 401 |
+
+---
+
+## DemoAuthenticator (текущий основной режим)
+
+Принимает **любую строку ≥6 символов** как логин. Не ходит во внешние сервисы.
+
+```
+login (≥6 символов)
+ │
+ ▼
+Sub = UUID v5(login) ← детерминированный, фиксированный UUID namespace
+Email = "demo-lo***in" ← маскированный для отображения в UI
+ │
+ ▼
+Namespace = "fission-" + hex(SHA256(Sub)[:8])
+```
+
+Пример:
+- login = `livetest@test.local`
+- Sub = `uuidV5("livetest@test.local")` = фиксированный UUID
+- Namespace = `fission-c3fce59430e41b0f`
+
+Каждый логин → один и тот же namespace (детерминированно).
+
+---
+
+## DeckAuthenticator (production JWT)
+
+Для JWT-токенов из облака NUBES:
+
+```
+JWT токен
+ │
+ ├─► validateToken → GET {deckAPI}/index.cfm/instances
+ │ Authorization: Bearer
+ │ 401 → ошибка | другой → ОК
+ │ Кэш на 5 минут (sync.Map, ключ "env:token")
+ │
+ └─► identityFromJWT → decode payload (base64url, без верификации подписи)
+ → claim "sub" → Sub
+ → claim "email" → Email (опционально)
+```
+
+Deck API URL по стендам:
+- `prod` → `https://deck-api.ngcloud.ru/api/v1`
+- `dev` → `https://deck-api-dev.ngcloud.ru/api/v1`
+- `test` → `https://deck-api-test.ngcloud.ru/api/v1`
+
+---
+
+## TestAuthenticator (тест-режим, устаревший)
+
+Принимает JWT — декодирует sub из payload. Или строку с `@` — использует как sub напрямую.
+Не ходит в Deck API. Включался через `FISSION_TEST_MODE=true`.
+
+> ⚠️ Сейчас не используется — заменён на DemoAuthenticator.
+
+---
+
+## Вычисление namespace: `NamespaceForSub`
+
+```go
+func NamespaceForSub(sub string) string {
+ h := sha256.Sum256([]byte(sub))
+ return "fission-" + hex.EncodeToString(h[:8]) // 16 hex символов
+}
+```
+
+Namespace **детерминирован**: одинаковый логин → всегда один namespace.
+
+---
+
+## authMiddleware — что происходит на каждый запрос
+
+```
+Входящий HTTP запрос
+ │
+ ├─► authTokenFromRequest → X-Auth-Token || Authorization: Bearer
+ │
+ ├─► authenticator.Authenticate(token, env) → UserIdentity{Sub, Email}
+ │ ошибка → 401
+ │
+ ├─► NamespaceForSub(Sub) → "fission-XXXXXXXXXXXXXXXX"
+ │
+ ├─► nsManager.EnsureUserNS(namespace)
+ │ → создаёт K8s namespace + RBAC если нет
+ │ ошибка → 502
+ │
+ └─► контекст запроса: ctxKeyNS=namespace, ctxKeyIdentity=identity
+ → handler работает в namespace пользователя
+```
+
+---
+
+## POST /auth — явный вход
+
+```
+POST /console/api/auth
+Body: { "token": "...", "env": "test" }
+ │
+ └─► то же что authMiddleware, возвращает:
+ { ok: true, namespace: "fission-...", email: "demo-lo***in" }
+```
+
+---
+
+## Итого — полная схема (актуальная)
+
+```
+Пользователь передаёт токен/логин
+ │
+ ▼
+MultiAuthenticator
+ ├── JWT? → DeckAuthenticator (Deck API + JWT decode)
+ └── нет → DemoAuthenticator (≥6 символов → UUID v5)
+ │
+ ▼
+UserIdentity { Sub, Email }
+ │
+ ▼
+NamespaceForSub(Sub) = "fission-" + hex(SHA256(Sub)[:8])
+ │
+ ▼
+EnsureUserNS → K8s namespace существует
+ │
+ ▼
+Все хендлеры работают с namespace из context
+```
+
+---
+
+# ЛЕГАСИ (до апреля 2026) — не удалять, для анализа
+
+
+
## Что вводит пользователь
В форме логина (`index.html`, overlay `#login-overlay`):
diff --git a/doc/descriptions/function-create-flow.md b/doc/descriptions/function-create-flow.md
new file mode 100644
index 0000000..767fad3
--- /dev/null
+++ b/doc/descriptions/function-create-flow.md
@@ -0,0 +1,180 @@
+# Создание функции — текущий flow (май 2026)
+
+## UI (браузер)
+
+### Форма создания (`create-modal`)
+
+Поля в `index.html`:
+- `c-lang` — select (python / nodejs / go / ruby / php)
+- `c-name` — имя функции
+- `c-entry` — entrypoint (заполняется автоматически по языку из `LANG_TEMPLATES`)
+- `c-route` — HTTP-путь
+- `c-methods` — методы, через запятую
+- `c-timeout` — таймаут в секундах
+- `c-schedule-enabled` + `c-cron` — cron-расписание (опционально)
+- `c-code` — **textarea** с исходным кодом функции (единственный способ передать код)
+
+Кнопки рядом с `c-code`:
+- `aiCheck(...)` — линтер (POST `/console/api/ai/check`)
+- `aiExplain(...)` — объяснение через LLM (POST `/console/api/ai/ask`)
+
+При нажатии «Создать» вызывается `submitCreate()` (`js/functions.js`):
+
+```js
+await requestJSON(API_BASE + '/functions', 'POST', {
+ name, language, entrypoint, route, methods, timeout,
+ code: document.getElementById('c-code').value // <-- всегда текст
+});
+```
+
+### Форма редактирования (`edit-modal`)
+
+Аналогична. `submitEdit()` шлёт:
+
+```js
+await requestJSON(API_BASE + '/functions/' + name + '/code', 'PUT', {
+ code: document.getElementById('e-code').value,
+ timeout: ...
+});
+```
+
+---
+
+## Backend
+
+### POST /console/api/functions → `handleCreateFunction`
+
+Структура запроса (`model.CreateFunctionRequest`):
+```json
+{ "name": "fn", "language": "python", "code": "def main(): ...",
+ "entrypoint": "main", "route": "/ns/fn", "methods": ["GET"],
+ "timeout": 60, "ttl": "" }
+```
+
+Обязательные: `name`, `language` (или `environment`), `code`.
+
+**Путь для интерпретируемых языков** (python / nodejs / ruby / php):
+```
+code (строка)
+ → buildDeployArchive(lang, code)
+ python/ruby/php: []byte(code) (raw, не zip)
+ nodejs: BuildJSDeployZip(code) → zip с package.json + main.js
+ → buildDeploySpec(ctx, bytes)
+ → uploadToStoragesvc() → multipart POST → storagesvc → archiveURL
+ → fallback: base64 literal (если storagesvc недоступен)
+ → Package.spec.deployment = { type: "url", url: "..." }
+ или = { type: "literal", literal: "base64..." }
+```
+
+**Путь для Go**:
+```
+code
+ → BuildGoSourceZip(code) → zip с исходником
+ → uploadToStoragesvc → archiveURL
+ → Package.spec.source = { ... } (source package, компилируется builder job)
+ Package.spec.buildcommand = "build"
+```
+
+После создания Package → создаётся Function → HTTPTrigger.
+При ошибке на любом шаге — откат (best-effort DELETE предыдущих объектов).
+
+### PUT /console/api/functions/{name}/code → `handleUpdateFunctionCode`
+
+```json
+{ "code": "...", "timeout": 60 }
+```
+
+Обязателен: `code`.
+
+Алгоритм:
+1. Читает текущую Function → берёт `fission-console/language` из аннотации
+2. `buildDeployArchive(lang, code)` → байты
+3. Создаёт **новый Package** с именем `{name}-pkg-{timestamp_base36}`
+4. Обновляет Function.spec.package.packageref на новый пакет
+5. Удаляет старый Package (best-effort)
+
+> Почему новый Package, а не обновление старого:
+> executor кэширует function-service по `functionUid` — не видит изменений в том же Package.
+> Новое имя гарантирует cache miss.
+
+---
+
+## Линтер (ai/check) и LLM (ai/ask)
+
+### POST /console/api/ai/check
+
+```json
+{ "language": "python", "code": "def main(): ..." }
+```
+
+Работает **только со строкой кода**. Запускает:
+- python: `python3 -m py_compile `
+- nodejs: `node --check `
+- ruby: `ruby -c `
+- php: `php -l `
+- go: `go/parser.ParseFile(...)` прямо в процессе (без tmpfile)
+
+Временный файл создаётся в `os.TempDir()`, удаляется через `defer`.
+
+### POST /console/api/ai/ask
+
+Отправляет код в LLM (объяснение, анализ). Детали в `ai_prompts.go`.
+
+---
+
+## Что сейчас НЕ поддерживается
+
+- **Загрузка архива (.zip) из файла** — ни в UI, ни в API
+- Архив можно только скачать (через `extractPackageSourceCode` → `fetchPackageArchive`)
+
+---
+
+## Planned: загрузка архива
+
+### API
+
+Добавить в `CreateFunctionRequest` и `UpdateCodeRequest` поле `archive_b64 string`.
+Логика в handler: если `archive_b64 != ""` — пропустить `buildDeployArchive`,
+сразу `uploadToStoragesvc(base64.Decode(archive_b64))`.
+
+Валидация:
+- magic bytes `PK\x03\x04` (zip)
+- максимум 10MB (против 1MB для кода)
+
+### UI
+
+Раздельные режимы в форме — кнопки-переключатели «✏️ Код» / «📦 Архив»:
+- режим «Код» — textarea (как сейчас)
+- режим «Архив» — ``, textarea скрыта
+
+В `submitCreate` / `submitEdit` — `FileReader.readAsDataURL(file)` → base64 → поле `archive_b64`.
+
+### Линтер и LLM для архива
+
+Проблема: линтер (`ai/check`) и LLM (`ai/ask`) принимают `code` — строку.
+Архив — zip из нескольких файлов. Варианты:
+
+**Вариант A — на бэке, при загрузке**
+После распаковки архива (уже есть `decodeArchiveBytesToSource` в `package.go`):
+- Извлечь главный файл (entrypoint)
+- Прогнать через линтер
+- Результат вернуть в том же ответе на create/update (поле `lint_result`)
+
+Плюсы: не меняет контракт линтера, нет доп. round-trip.
+Минусы: линтится только один файл из архива.
+
+**Вариант B — на фронте, до отправки**
+`FileReader.readAsText(file)` — не работает для zip напрямую.
+Можно использовать `JSZip` (npm/CDN) — распаковать в браузере → взять главный файл → передать в `ai/check`.
+
+Плюсы: линтинг до загрузки, быстрая обратная связь.
+Минусы: добавляется зависимость (JSZip ~100KB).
+
+**Вариант C — гибрид (рекомендован)**
+1. Фронт: при выборе архива — использовать JSZip, найти entrypoint-файл по языку,
+ показать его в readonly-textarea, кнопки линтера и LLM работают с этим текстом.
+2. Отправка: `archive_b64` (весь zip) идёт на бэк как есть.
+3. Бэк (опционально): после сохранения прогнать `decodeArchiveBytesToSource` → линтер,
+ вернуть предупреждение если нашёл ошибки.
+
+Так пользователь видит и код и результат линтера до деплоя, и полный архив уходит без изменений.
diff --git a/scripts/run_remaining.sh b/scripts/run_remaining.sh
new file mode 100644
index 0000000..221bf75
--- /dev/null
+++ b/scripts/run_remaining.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+# run_remaining.sh — ВРЕМЕННЫЙ: только test_stress.sh (после обрыва SSH)
+# Удалить после прогона.
+set -uo pipefail
+
+ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
+
+LOG_DIR="${ROOT_DIR}/../test-results"
+mkdir -p "$LOG_DIR"
+LOG_FILE="${LOG_DIR}/$(date +%Y-%m-%d_%H-%M-%S)_remaining.log"
+if [ -z "${_LOGGING:-}" ]; then
+ export _LOGGING=1
+ exec bash "$0" "$@" 2>&1 | tee "$LOG_FILE"
+ exit $?
+fi
+echo "LOG: $LOG_FILE"
+
+bash "${ROOT_DIR}/test_stress.sh"
+EXIT=$?
+echo ""
+if [ $EXIT -eq 0 ]; then
+ echo " ✓ test_stress.sh — PASSED"
+else
+ echo " ✗ test_stress.sh — FAILED (exit $EXIT)"
+fi
+exit $EXIT
diff --git a/scripts/test_stress.sh b/scripts/test_stress.sh
index 5144207..51204ac 100755
--- a/scripts/test_stress.sh
+++ b/scripts/test_stress.sh
@@ -129,8 +129,10 @@ for i in $(seq 1 "$INVOKE_COUNT"); do
-H "X-Auth-Token: ${INVOKE_SUB}" -H "Content-Type: application/json" -d '{}')
ST=$(json_field "$R" status)
[ "$ST" = "200" ] && break
+ echo " прогрев функции $i/$INVOKE_COUNT: попытка $try/12..."
sleep 5
done
+ echo " функция $i/$INVOKE_COUNT — готова"
done
for i in $(seq 1 "$INVOKE_COUNT"); do