feat(apps): stepped creation wizard, branch previews, and app-creation fixes
This session (frontend focus):
- Rebuild /apps/new as a 4-step wizard (Basics → Configure → Trigger → Review):
WizardRail, SourceKindPicker card grid, AppManifest review, per-step validation,
ConfirmDialog-based unsaved-changes guard.
- Extract lib/workload/sourceForms.ts (single source of truth for source_config)
+ {Image,Compose,Static,Dockerfile}SourceForm + StaticDiscoveryWizard; fold the
/apps/[id] edit form onto the same components (removes the duplication). Add
vitest + sourceForms unit tests.
- Branch preview environments UI: /chain is_preview/preview_branch + a Preview
environments panel on /apps/[id] (per-branch URLs, ConfirmDialog teardown, armed
state); RegistryImagePicker on the registry trigger and the image source.
- Fixes: image-inspect 404 -> admin-gated POST /api/discovery/image/inspect;
conflict-panel blur flicker; friendly localized discovery errors; CPU/Memory
label hints; dashboard + /apps "Total workloads" count only source_kind workloads
(drop stale trigger_kind gate); NPM cert/access-list name cache; EntityPicker
empty-list guard.
- Update CLAUDE.md frontend conventions + add a Build & Test section.
Also captures pre-existing in-progress platform work (not from this session):
workload notifications, Prometheus metrics export, store lockfile, health probes,
backup hardening, and related store/webhook/scheduler changes.
This commit is contained in:
+143
-15
@@ -15,7 +15,7 @@
|
||||
"nav": {
|
||||
"dashboard": "Панель",
|
||||
"apps": "Приложения",
|
||||
"eventTriggers": "Триггеры",
|
||||
"eventTriggers": "Триггеры событий",
|
||||
"logScanRules": "Лог-правила",
|
||||
"triggers": "Триггеры",
|
||||
"proxies": "Прокси",
|
||||
@@ -23,7 +23,13 @@
|
||||
"settings": "Настройки",
|
||||
"logout": "Выйти",
|
||||
"dns": "DNS-записи",
|
||||
"containers": "Контейнеры"
|
||||
"containers": "Контейнеры",
|
||||
"sectionObserve": "Наблюдение",
|
||||
"sectionSystem": "Система",
|
||||
"closeSidebar": "Закрыть боковую панель",
|
||||
"openSidebar": "Открыть боковую панель",
|
||||
"quickNavTitle": "Нажмите «g», затем букву для перехода между разделами",
|
||||
"quickNavLabel": "быстрая навигация"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Панель управления",
|
||||
@@ -42,7 +48,11 @@
|
||||
"systemHealth": "Состояние системы",
|
||||
"daemons": "Демоны",
|
||||
"systemResources": "Системные ресурсы",
|
||||
"systemResourcesSubtitle": "CPU, память, диск и топ потребителей"
|
||||
"systemResourcesSubtitle": "CPU, память, диск и топ потребителей",
|
||||
"statSubWorkloads": "нагрузки →",
|
||||
"statSubRunning": "запущено",
|
||||
"statSubNeedAttention": "требует внимания",
|
||||
"statSubStale": "устаревшие →"
|
||||
},
|
||||
"resources": {
|
||||
"cpuCores": "Ядра CPU",
|
||||
@@ -237,6 +247,7 @@
|
||||
"deleteFailed": "Не удалось удалить реестр",
|
||||
"testFailed": "Тест подключения не удался",
|
||||
"loadFailed": "Не удалось загрузить реестры",
|
||||
"deleteTitle": "Удалить реестр?",
|
||||
"deleteConfirm": "Удалить реестр «{name}»? Это действие необратимо.",
|
||||
"healthChecking": "Проверка...",
|
||||
"healthConnected": "Подключено",
|
||||
@@ -354,6 +365,7 @@
|
||||
"createFailed": "Не удалось создать пользователя",
|
||||
"deleteFailed": "Не удалось удалить пользователя",
|
||||
"deleteConfirm": "Вы уверены, что хотите удалить этого пользователя?",
|
||||
"deleteConfirmMessage": "Удалить пользователя «{username}»? Это действие необратимо.",
|
||||
"usernameRequired": "Имя пользователя и пароль обязательны",
|
||||
"networkError": "Ошибка сети",
|
||||
"password": "Пароль"
|
||||
@@ -400,6 +412,9 @@
|
||||
"common": {
|
||||
"cancel": "Отмена",
|
||||
"confirm": "Подтвердить",
|
||||
"close": "Закрыть",
|
||||
"toggle": "Переключить",
|
||||
"dismissNotification": "Закрыть уведомление",
|
||||
"delete": "Удалить",
|
||||
"edit": "Изменить",
|
||||
"change": "Изменить",
|
||||
@@ -429,6 +444,7 @@
|
||||
"missing": "Отсутствует"
|
||||
},
|
||||
"containers": {
|
||||
"eyebrowSuffix": "ГЛОБАЛЬНО",
|
||||
"errLoad": "Не удалось загрузить контейнеры",
|
||||
"searchPlaceholder": "Поиск по нагрузке, роли, образу, поддомену…",
|
||||
"kindFilterLabel": "Тип нагрузки",
|
||||
@@ -476,6 +492,7 @@
|
||||
},
|
||||
"stale": {
|
||||
"title": "Устаревшие контейнеры",
|
||||
"eyebrowSuffix": "УСТАРЕВШИЕ",
|
||||
"noStale": "Нет устаревших контейнеров",
|
||||
"noStaleDesc": "Все контейнеры исправны и работают.",
|
||||
"cleanup": "Очистить",
|
||||
@@ -541,13 +558,13 @@
|
||||
"unavailable": "Статистика недоступна"
|
||||
},
|
||||
"systemHealth": {
|
||||
"title": "Состояние системы",
|
||||
"containers": "Контейнеры",
|
||||
"proxies": "Прокси",
|
||||
"recentErrors": "Недавние ошибки"
|
||||
},
|
||||
"daemons": {
|
||||
"title": "Демоны",
|
||||
"notReachable": "{provider} недоступен.",
|
||||
"refresh": "Обновить",
|
||||
"refreshing": "Обновление",
|
||||
"docker": "Docker Engine",
|
||||
@@ -1110,6 +1127,10 @@
|
||||
"image": "Ссылка на образ",
|
||||
"imagePlaceholder": "registry.example.com/owner/app",
|
||||
"imageHint": "Полная ссылка на образ без тега — Tinyforge ловит новые теги, выкладываемые под этой ссылкой.",
|
||||
"browseImages": "Выбрать",
|
||||
"browseImagesHint": "Выберите образ из настроенного реестра вместо ручного ввода ссылки.",
|
||||
"browseImagesTitle": "Выбор образа",
|
||||
"browseImagesSearch": "Поиск образов…",
|
||||
"tagPattern": "Шаблон тега",
|
||||
"tagPatternPlaceholder": "*",
|
||||
"tagPatternHint": "Glob path.Match (например, v*, release-*). * совпадает с любым тегом.",
|
||||
@@ -1122,6 +1143,9 @@
|
||||
"branch": "Ветка",
|
||||
"branchPlaceholder": "main",
|
||||
"branchHint": "Только push'и, продвигающие эту ветку, дёргают триггер.",
|
||||
"branchPattern": "Шаблон ветки (preview-деплои)",
|
||||
"branchPatternPlaceholder": "feat/* или * для любой ветки",
|
||||
"branchPatternHint": "Если задан, любой push в подходящую ветку создаёт отдельный preview-деплой. Оставьте пустым, чтобы выключить.",
|
||||
"manualNote": "У ручных триггеров нет конфига. Они срабатывают только через кнопку Deploy на странице нагрузки или POST /workloads/{id}/deploy.",
|
||||
"scheduleNote": "Срабатывает по фиксированному интервалу, который ведёт внутренний планировщик Tinyforge. Внешний webhook не нужен — включите его ниже только если CI тоже должен запускать триггер вручную.",
|
||||
"intervalPresets": "Быстрые пресеты",
|
||||
@@ -1186,6 +1210,14 @@
|
||||
},
|
||||
"new": {
|
||||
"pageTitle": "Новое приложение · Tinyforge",
|
||||
"wizard": {
|
||||
"stepBasics": "Основное",
|
||||
"stepConfigure": "Настройка",
|
||||
"stepTrigger": "Триггер",
|
||||
"stepReview": "Обзор",
|
||||
"next": "Далее",
|
||||
"back": "Назад"
|
||||
},
|
||||
"backLabel": "К приложениям",
|
||||
"eyebrowSuffix": "НОВОЕ ПРИЛОЖЕНИЕ",
|
||||
"title": "Создать приложение",
|
||||
@@ -1198,6 +1230,7 @@
|
||||
"alertTag": "ОШ",
|
||||
"fieldName": "Имя",
|
||||
"fieldNameRequired": "ОБЯЗАТЕЛЬНО",
|
||||
"fieldRequired": "Обязательно",
|
||||
"fieldNamePlaceholder": "my-app",
|
||||
"fieldNameHint": "В нижнем регистре, без пробелов. Используется в именах контейнеров и поддоменах.",
|
||||
"fieldSourcePlugin": "Source-плагин",
|
||||
@@ -1207,7 +1240,7 @@
|
||||
"fieldConfigYaml": "YAML",
|
||||
"fieldConfigForm": "ФОРМА",
|
||||
"fieldConfigJson": "JSON",
|
||||
"advancedJson": "Расширенный JSON",
|
||||
"advancedJson": "Редактировать JSON",
|
||||
"backToForm": "К форме",
|
||||
"resetSample": "Сбросить к примеру",
|
||||
"switchToJsonTitle": "Переключиться на сырой JSON-редактор",
|
||||
@@ -1230,11 +1263,21 @@
|
||||
"imageRegistryLabel": "Реестр (для приватных pull-ов)",
|
||||
"imageRegistryPublic": "(публичный — без авторизации)",
|
||||
"imageRegistryHint": "Имя должно совпадать с записью на странице «Реестры». Оставьте пустым для публичных образов.",
|
||||
"imageCpu": "Лимит CPU (ядра, 0 = ∞)",
|
||||
"imageMemory": "Лимит памяти (МБ, 0 = ∞)",
|
||||
"imageCpu": "Лимит CPU",
|
||||
"imageCpuHint": "Ядра, 0 = ∞",
|
||||
"imageMemory": "Лимит памяти",
|
||||
"imageMemoryHint": "МБ, 0 = ∞",
|
||||
"imageMax": "Макс. инстансов",
|
||||
"imageMaxHint": "1 = строгий blue-green.",
|
||||
"imageFoot": "Переменные окружения и тома задаются в отдельных панелях на странице нагрузки после создания.",
|
||||
"dockerfileHeader": "dockerfile-источник · сборка из git-репозитория",
|
||||
"dockerfileBuildEyebrow": "сборка · dockerfile",
|
||||
"dockerfileContextPath": "Контекст сборки",
|
||||
"dockerfileContextPathPlaceholder": "(пусто = корень репо)",
|
||||
"dockerfilePath": "Путь к Dockerfile",
|
||||
"dockerfilePort": "Порт контейнера",
|
||||
"dockerfilePortRequired": "Укажите порт, который слушает приложение (1–65535).",
|
||||
"dockerfileFoot": "Tinyforge склонирует репо, соберёт образ из Dockerfile и запустит контейнер. Переменные окружения и тома — на странице нагрузки после создания.",
|
||||
"staticHeader": "static-источник · страницы из репозитория",
|
||||
"staticProvider": "Провайдер",
|
||||
"staticBaseUrl": "Base URL",
|
||||
@@ -1262,7 +1305,7 @@
|
||||
"staticTestConnection": "Проверить соединение",
|
||||
"staticConnectionOk": "Соединение установлено",
|
||||
"staticConnectionFailed": "Ошибка соединения: {error}",
|
||||
"staticBrowseRepos": "Выбрать репозиторий",
|
||||
"staticBrowseRepos": "Обзор",
|
||||
"staticBrowseBranches": "Выбрать ветку",
|
||||
"staticBrowseFolders": "Выбрать папку",
|
||||
"staticPickerRepoTitle": "Выбор репозитория",
|
||||
@@ -1275,6 +1318,7 @@
|
||||
"staticTreeEmpty": "В этой ветке нет папок.",
|
||||
"staticDenoAutoDetected": "Обнаружена папка <code>api/</code> — режим автоматически переключён на Deno.",
|
||||
"imageConflictTag": "ОБРАЗ УЖЕ ИСПОЛЬЗУЕТСЯ",
|
||||
"imageConflictChecking": "Проверка конфликтов…",
|
||||
"imageConflictHeading": "Этот образ уже используется в {count} нагрузке(ах):",
|
||||
"imageConflictOpenBtn": "Открыть",
|
||||
"imageConflictAcknowledgeNote": "Если это намеренно (например, отдельный этап), нажмите «Создать» ещё раз для продолжения.",
|
||||
@@ -1300,21 +1344,23 @@
|
||||
"submit": "Создать приложение",
|
||||
"submitting": "Создание…",
|
||||
"submitAnyway": "Всё равно создать",
|
||||
"unsavedChanges": "В этом приложении есть несохранённые изменения. Покинуть страницу, не создавая его?",
|
||||
"unsavedChangesTitle": "Несохранённые изменения",
|
||||
"unsavedChangesConfirm": "Покинуть",
|
||||
"errors": {
|
||||
"detectionFailed": "Не удалось определить провайдера.",
|
||||
"connectionFailed": "Ошибка соединения.",
|
||||
"reposFailed": "Не удалось загрузить репозитории.",
|
||||
"branchesFailed": "Не удалось загрузить ветки.",
|
||||
"treeFailed": "Не удалось загрузить дерево папок.",
|
||||
"detectionFailed": "Не удалось определить Git-провайдера по этому URL. Проверьте, что базовый URL верен и доступен.",
|
||||
"connectionFailed": "Не удалось подключиться к репозиторию. Проверьте URL провайдера, владельца/репозиторий и токен доступа (для приватных репозиториев).",
|
||||
"reposFailed": "Не удалось получить список репозиториев. Проверьте базовый URL и токен доступа.",
|
||||
"branchesFailed": "Не удалось получить список веток. Проверьте репозиторий и токен доступа.",
|
||||
"treeFailed": "Не удалось загрузить дерево папок. Проверьте репозиторий, ветку и токен доступа.",
|
||||
"sourceConfigInvalid": "source_config не является корректным JSON.",
|
||||
"triggerBindUnknown": "неизвестная ошибка",
|
||||
"createFailed": "Не удалось создать нагрузку.",
|
||||
"inspectFailed": "Не удалось проинспектировать образ."
|
||||
"inspectFailed": "Не удалось проинспектировать образ — убедитесь, что он скачан локально и ссылка указана верно."
|
||||
},
|
||||
"imageInspect": "Инспектировать",
|
||||
"imageInspectHint": "Подставляет порт и healthcheck из образа, чтобы не вводить вручную.",
|
||||
"imageInspectOk": "Готово — порт и healthcheck подставлены.",
|
||||
"imageInspectError": "Ошибка инспекции: {error}",
|
||||
"triggers": {
|
||||
"section": "Триггер",
|
||||
"sectionSub": "Необязательно. Выберите, откуда придёт сигнал передеплоя — слежение за реестром, git-событие или ручная кнопка.",
|
||||
@@ -1334,6 +1380,18 @@
|
||||
"pickWebhookOn": "ВЕБХУК ВКЛ",
|
||||
"skippedNote": "Триггер не будет привязан. Добавьте его из панели «Триггеры» в карточке приложения после создания.",
|
||||
"bindError": "Приложение создано, но привязка триггера не удалась: {error}. Откройте панель «Триггеры» в карточке, чтобы повторить."
|
||||
},
|
||||
"manifest": {
|
||||
"title": "Манифест",
|
||||
"name": "Имя",
|
||||
"source": "Источник",
|
||||
"trigger": "Триггер",
|
||||
"publicFace": "Публичный фронт",
|
||||
"unnamed": "(без имени)",
|
||||
"registryPublic": "публичный реестр",
|
||||
"folderRoot": "корень",
|
||||
"triggerManual": "Только вручную",
|
||||
"internalOnly": "Только внутренний"
|
||||
}
|
||||
},
|
||||
"detail": {
|
||||
@@ -1365,6 +1423,40 @@
|
||||
"unavailable": "Не удалось получить размер (контейнер мог быть остановлен).",
|
||||
"loading": "Вычисление размера…"
|
||||
},
|
||||
"buildLog": {
|
||||
"title": "Журнал сборки",
|
||||
"sub": "Живой поток вывода сборки Docker.",
|
||||
"clear": "Очистить"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Маршруты уведомлений",
|
||||
"sub": "Множественные точки доставки для событий деплоя/сборки. При пустом списке используется устаревший единственный URL.",
|
||||
"loading": "Загрузка маршрутов…",
|
||||
"empty": "Нет настроенных маршрутов уведомлений. Добавьте, чтобы получать события в отдельный канал.",
|
||||
"addFirst": "Добавить первый маршрут",
|
||||
"add": "Добавить маршрут",
|
||||
"edit": "Изменить",
|
||||
"delete": "Удалить",
|
||||
"name": "Имя",
|
||||
"namePlaceholder": "Slack #alerts",
|
||||
"url": "URL вебхука",
|
||||
"secret": "Секрет подписи",
|
||||
"secretPlaceholder": "Опционально — приёмник проверяет HMAC",
|
||||
"secretEditPlaceholder": "Оставьте пустым, чтобы сохранить текущий секрет",
|
||||
"secretHint": "HMAC-SHA256 от тела запроса, заголовок X-Hub-Signature-256.",
|
||||
"eventTypes": "Типы событий",
|
||||
"eventTypesPlaceholder": "deploy_failure,build_failure (пусто = все)",
|
||||
"eventTypesHint": "Список через запятую. Пусто — маршрут срабатывает на любое событие.",
|
||||
"enabled": "Включён",
|
||||
"save": "Сохранить",
|
||||
"saving": "Сохранение…",
|
||||
"cancel": "Отмена",
|
||||
"allEvents": "все события",
|
||||
"signed": "подписан",
|
||||
"disabled": "выключен",
|
||||
"confirmDeleteTitle": "Удалить маршрут уведомлений?",
|
||||
"confirmDeleteMessage": "Маршрут перестанет срабатывать. Устаревший URL уведомлений на workload (если задан) снова возьмёт события на себя."
|
||||
},
|
||||
"toolbar": {
|
||||
"stop": "Стоп",
|
||||
"start": "Старт",
|
||||
@@ -1455,6 +1547,19 @@
|
||||
"editStaticModeDenoDesc": "— Deno-рантайм с динамической маршрутизацией.",
|
||||
"editStaticRenderMarkdown": "Рендерить markdown",
|
||||
"editStaticRenderMarkdownDesc": "— автоматически отдавать <code>.md</code> как HTML.",
|
||||
"editDockerfileHeader": "dockerfile-источник · сборка из git-репозитория",
|
||||
"editDockerfileBuildEyebrow": "сборка · dockerfile",
|
||||
"editDockerfileContextPath": "Контекст сборки",
|
||||
"editDockerfileContextPathPlaceholder": "(пусто = корень репо)",
|
||||
"editDockerfilePath": "Путь к Dockerfile",
|
||||
"editDockerfilePort": "Порт контейнера",
|
||||
"editTestConnection": "Проверить соединение",
|
||||
"editTestConnectionOk": "Соединение установлено",
|
||||
"editTestConnectionFailed": "Ошибка соединения: {error}",
|
||||
"editTestConnectionUnknownError": "Неизвестная ошибка",
|
||||
"overrideKeyUnitSingular": "КЛЮЧ",
|
||||
"overrideKeyUnitPlural": "КЛЮЧИ",
|
||||
"editTestConnectionIncomplete": "Заполните провайдера, base URL, owner и name.",
|
||||
"editSourceJsonHeader": "source_config.json",
|
||||
"editSourceJsonAria": "Конфигурация source-плагина (JSON)",
|
||||
"editPublicFaces": "Публичные фронты",
|
||||
@@ -1494,6 +1599,29 @@
|
||||
"chainPromoteButton": "Продвинуть от родителя",
|
||||
"chainPromoting": "Продвижение…",
|
||||
"chainHint": "Задайте <code>parent_workload_id</code> у нагрузки, чтобы построить цепочку. Дочерние image-источники могут одним кликом продвинуть текущий запущенный тег родителя.",
|
||||
"previews": {
|
||||
"title": "Превью-окружения",
|
||||
"subEmpty": "нет активных превью",
|
||||
"subCountOne": "1 активное превью",
|
||||
"subCount": "активных превью: {count}",
|
||||
"tag": "Превью",
|
||||
"tagTitle": "Превью-развёртывание этой нагрузки для отдельной ветки",
|
||||
"armedEmpty": "Нет активных превью — отправьте пуш в ветку, соответствующую шаблону",
|
||||
"noneEmpty": "Пока нет активных превью.",
|
||||
"open": "Открыть",
|
||||
"noUrl": "нет публичного URL",
|
||||
"teardown": "Удалить",
|
||||
"teardownTitle": "Удалить превью?",
|
||||
"teardownMessage": "Это удалит превью для ветки «{name}» вместе с его контейнерами и маршрутами прокси. Новый пуш в эту ветку создаст его заново.",
|
||||
"teardownConfirm": "Удалить",
|
||||
"teardownPending": "Удаление…",
|
||||
"teardownFailed": "Не удалось удалить",
|
||||
"stateRunning": "Работает",
|
||||
"statePending": "Запускается",
|
||||
"stateStopped": "Остановлено",
|
||||
"stateUnknown": "Неизвестно",
|
||||
"hint": "Превью создаются автоматически, когда пуш приходит в ветку, соответствующую <code>branch_pattern</code> git-триггера, и удаляются при удалении ветки. Каждое получает собственный поддомен с префиксом-слагом."
|
||||
},
|
||||
"volumesTitle": "Тома",
|
||||
"volumesEmpty": "Нет монтирований",
|
||||
"volumesCountSingular": "{count} монтирование",
|
||||
|
||||
Reference in New Issue
Block a user