chore(workload): close the workload-first arc — apps i18n + codemap + tests
Build / build (push) Successful in 10m36s
Build / build (push) Successful in 10m36s
Closes the workload-first refactor by landing the Priority 3 polish items and the Priority 4 test gap. Net: ~2,400 lines added, ~350 lines modified across 13 files. Priority 3 — polish - apps.* i18n namespace: 276 new keys across apps.list.* (27), apps.new.* (91, sibling of existing apps.new.triggers.*), and apps.detail.* (158, sibling of existing apps.detail.bindings.*). EN+RU at 1314 keys each, perfectly in sync. /apps, /apps/new, /apps/[id] now render entirely from i18n. - New codemap docs/CODEMAPS/workload-plugin.md (238 lines): Source × Trigger contract, dispatch seam, webhook fan-out path, recipes for adding a new Source or Trigger kind. Plus docs/CODEMAPS/INDEX.md gateway. Priority 4 — tests - internal/api/workloads_test.go (new, ~30 subtests): /api/workloads CRUD + deploy + delete + env + volumes + chain + promote-from + triggers list/inline-bind + auth gating + standalone /api/triggers CRUD (create / dup-409 / kind filter / delete). Uses real POST handlers via httptest.NewServer + a fake plugin source registered under "testfakesource". - internal/deployer/dispatch_test.go (new, 11 tests): DispatchPlugin / DispatchTeardown / DispatchReconcile happy + unknown-kind + propagated-error each; PluginDeps wiring; a real 2s-bounded RWMutex deadlock probe on PluginDeps vs SetDNSProvider. - internal/workload/plugin/source/compose/compose_test.go (new, ~26 subtests): composeProjectName sanitization, writeYAML / writeYAMLIfChanged hash short-circuit, Validate happy + bad inputs, Kind / SchemaSample. Coverage delta on the workload-plugin path: - internal/api: 1.1% → 16.0% - internal/deployer: 0% → 54.1% - internal/workload/plugin/source/compose: 0% → 38.5% - Trigger plugins already at 87-95% from the trigger-split work. Production fix surfaced by the tests - store.CreateWorkload now self-references RefID = ID when caller leaves RefID empty (the typical plugin-native path). The api layer's broken backfill loop (called UpdateWorkload, which deliberately omits ref_id) is gone. Multiple sibling plugin workloads can now coexist under the UNIQUE(kind, ref_id) constraint. Review fixes addressed before commit - CRITICAL: deadlock-detect test gained a real 2s time.After (was selecting on context.Background().Done() which never fires). - HIGH: happy-path test now hard-asserts RefID = ID (was a t.Logf that would silently pass after a production fix). - HIGH: standalone /api/triggers CRUD coverage added (was bypassed by the workload-side bind flow). - HIGH: seedWorkload bypass deleted; tests now go through the real POST /api/workloads handler. - MEDIUM: withTempDir restore is a no-op (t.Setenv auto-restores); dead `old := os.Getenv(...)` capture removed. - MEDIUM: list-workloads test now asserts ID membership, not just count. Doc - WORKLOAD_REFACTOR_TODO: all three Priority 1 items, Priority 3 polish, and Priority 4 tests marked DONE. The workload-first arc is closed.
This commit is contained in:
@@ -1128,7 +1128,127 @@
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
"list": {
|
||||
"eyebrowSuffix": "ПРИЛОЖЕНИЯ",
|
||||
"title": "Приложения",
|
||||
"ledePrefix": "Plugin-native деплои —",
|
||||
"ledeKindImage": "image",
|
||||
"ledeKindCompose": "compose",
|
||||
"ledeKindStatic": "static",
|
||||
"ledeMiddle": ", или",
|
||||
"ledeSuffix": ", с подключаемыми триггерами передеплоя. Старые проекты, стеки и сайты на время переезда остаются в собственных разделах.",
|
||||
"statTotal": "ВСЕГО",
|
||||
"statImage": "IMAGE",
|
||||
"statCompose": "COMPOSE",
|
||||
"statStatic": "STATIC",
|
||||
"refresh": "Обновить",
|
||||
"newApp": "Новое приложение",
|
||||
"filterAriaLabel": "Фильтр по source-плагину",
|
||||
"filterAll": "ВСЕ",
|
||||
"loadError": "Не удалось загрузить приложения",
|
||||
"alertTag": "ОШ",
|
||||
"emptyTitle": "Приложений пока нет",
|
||||
"emptyBody": "Приложения объединяют image, compose и static-деплои за единым plugin-driven интерфейсом. Создайте первое, чтобы увидеть его здесь.",
|
||||
"emptyCta": "Создать первое приложение",
|
||||
"colName": "Имя",
|
||||
"colSource": "Источник",
|
||||
"colTrigger": "Триггер",
|
||||
"colCreated": "Создано",
|
||||
"colActions": "Действия",
|
||||
"rowOpen": "Открыть"
|
||||
},
|
||||
"new": {
|
||||
"pageTitle": "Новое приложение · Tinyforge",
|
||||
"backLabel": "К приложениям",
|
||||
"eyebrowSuffix": "НОВОЕ ПРИЛОЖЕНИЕ",
|
||||
"title": "Создать приложение",
|
||||
"ledePrefix": "Создайте plugin-native нагрузку.",
|
||||
"ledeSourceLabel": "Источник",
|
||||
"ledeSourceMid": "= как она деплоится (image, compose, static). Выберите или создайте",
|
||||
"ledeTriggerLabel": "триггер",
|
||||
"ledeSuffix": "ниже — при срабатывании source-плагин передеплоит приложение.",
|
||||
"loadingKinds": "Загрузка доступных видов плагинов…",
|
||||
"alertTag": "ОШ",
|
||||
"fieldName": "Имя",
|
||||
"fieldNameRequired": "ОБЯЗАТЕЛЬНО",
|
||||
"fieldNamePlaceholder": "my-app",
|
||||
"fieldNameHint": "В нижнем регистре, без пробелов. Используется в именах контейнеров и поддоменах.",
|
||||
"fieldSourcePlugin": "Source-плагин",
|
||||
"fieldSourceLabel": "Источник",
|
||||
"fieldSourceHint": "Список берётся из запущенного демона — видны только вкомпилированные плагины. Триггеры (registry / git / manual) настраиваются ниже как отдельные записи.",
|
||||
"fieldSourceConfig": "Конфигурация источника",
|
||||
"fieldConfigYaml": "YAML",
|
||||
"fieldConfigForm": "ФОРМА",
|
||||
"fieldConfigJson": "JSON",
|
||||
"advancedJson": "Расширенный JSON",
|
||||
"backToForm": "К форме",
|
||||
"resetSample": "Сбросить к примеру",
|
||||
"switchToJsonTitle": "Переключиться на сырой JSON-редактор",
|
||||
"switchToFormTitle": "Вернуться к форме",
|
||||
"jsonOk": "JSON ОК",
|
||||
"jsonInvalid": "JSON НЕВЕРНЫЙ",
|
||||
"linesUnit": "строк",
|
||||
"composeHeader": "compose.yaml · compose",
|
||||
"composeAriaLabel": "Compose YAML",
|
||||
"composeProjectLabel": "Имя compose-проекта (опционально)",
|
||||
"composeProjectPlaceholder": "(по умолчанию — нормализованное имя нагрузки)",
|
||||
"composePlaceholder": "services:\n web:\n image: nginx:alpine\n ports:\n - \"80\"",
|
||||
"imageHeader": "image-источник · параметры рантайма",
|
||||
"imageRefLabel": "Образ (путь в реестре)",
|
||||
"imageRefPlaceholder": "registry.example.com/owner/app",
|
||||
"imageRefHint": "Полная ссылка без тега; тег задаётся при деплое — триггером или полем «Тег по умолчанию» ниже.",
|
||||
"imagePort": "Порт",
|
||||
"imageHealthcheck": "Путь healthcheck",
|
||||
"imageDefaultTag": "Тег по умолчанию",
|
||||
"imageRegistryLabel": "Реестр (для приватных pull-ов)",
|
||||
"imageRegistryPublic": "(публичный — без авторизации)",
|
||||
"imageRegistryHint": "Имя должно совпадать с записью на странице «Реестры». Оставьте пустым для публичных образов.",
|
||||
"imageCpu": "Лимит CPU (ядра, 0 = ∞)",
|
||||
"imageMemory": "Лимит памяти (МБ, 0 = ∞)",
|
||||
"imageMax": "Макс. инстансов",
|
||||
"imageMaxHint": "1 = строгий blue-green.",
|
||||
"imageFoot": "Переменные окружения и тома задаются в отдельных панелях на странице нагрузки после создания.",
|
||||
"staticHeader": "static-источник · страницы из репозитория",
|
||||
"staticProvider": "Провайдер",
|
||||
"staticBaseUrl": "Base URL",
|
||||
"staticBaseUrlPlaceholder": "https://git.example.com",
|
||||
"staticRepoOwner": "Владелец репозитория",
|
||||
"staticRepoOwnerPlaceholder": "owner",
|
||||
"staticRepoName": "Имя репозитория",
|
||||
"staticRepoNamePlaceholder": "pages",
|
||||
"staticBranch": "Ветка",
|
||||
"staticBranchPlaceholder": "main",
|
||||
"staticFolder": "Папка (опционально)",
|
||||
"staticFolderPlaceholder": "(корень репозитория)",
|
||||
"staticToken": "Токен доступа (приватные репозитории)",
|
||||
"staticTokenPlaceholder": "(оставьте пустым для публичных репозиториев)",
|
||||
"staticTokenHint": "Шифруется при хранении. Нужен только для приватных репозиториев.",
|
||||
"staticMode": "Режим",
|
||||
"staticModeStaticDesc": "— раздача файлов через nginx; ноль рантайм-оверхеда.",
|
||||
"staticModeDenoDesc": "— Deno-рантайм с опциональной динамической маршрутизацией.",
|
||||
"staticRenderMarkdown": "Рендерить markdown",
|
||||
"staticRenderMarkdownDesc": "— автоматически отдавать <code>.md</code> файлы как HTML-страницы.",
|
||||
"staticFoot": "Секрет вебхука для git push-триггеров появляется в панели вебхука нагрузки после создания.",
|
||||
"sourceConfigJsonTitle": "source_config.json · {kind}",
|
||||
"sourceConfigJsonAria": "Конфигурация source-плагина (JSON)",
|
||||
"triggerNumLabel": "Триггер",
|
||||
"triggerNumOptional": "ОПЦИОНАЛЬНО",
|
||||
"triggerNewTag": "НОВЫЙ",
|
||||
"triggerPickTag": "ВЫБРАТЬ",
|
||||
"triggerSkipTag": "ПРОПУСК",
|
||||
"noteSkipTag": "ПРОПУСК",
|
||||
"noteEmptyTag": "∅",
|
||||
"faceLabel": "Публичный фронт",
|
||||
"faceOptional": "ОПЦИОНАЛЬНО",
|
||||
"faceSubdomain": "Поддомен",
|
||||
"faceSubdomainPlaceholder": "myapp",
|
||||
"faceDomain": "Домен",
|
||||
"faceDomainPlaceholder": "(наследуется из настроек)",
|
||||
"facePort": "Целевой порт",
|
||||
"faceHint": "Оставьте пустым, чтобы не создавать прокси-маршрут. Заполнение любого поля создаст одну запись фронта, привязанную к этой нагрузке.",
|
||||
"cancel": "Отмена",
|
||||
"submit": "Создать приложение",
|
||||
"submitting": "Создание…",
|
||||
"triggers": {
|
||||
"section": "Триггер",
|
||||
"sectionSub": "Необязательно. Выберите, откуда придёт сигнал передеплоя — слежение за реестром, git-событие или ручная кнопка.",
|
||||
@@ -1151,6 +1271,160 @@
|
||||
}
|
||||
},
|
||||
"detail": {
|
||||
"pageTitleFallback": "Приложение",
|
||||
"backLabel": "К приложениям",
|
||||
"eyebrowSuffix": "ПРИЛОЖЕНИЕ",
|
||||
"kickerId": "id: {id}",
|
||||
"loading": "Загрузка нагрузки…",
|
||||
"loadError": "Не удалось загрузить приложение",
|
||||
"deployError": "Деплой не удался",
|
||||
"saveError": "Сохранение не удалось",
|
||||
"deleteError": "Удаление не удалось",
|
||||
"alertTag": "ОШ",
|
||||
"createdAt": "создано",
|
||||
"refreshLabel": "Обновить",
|
||||
"editButton": "Изменить",
|
||||
"deleteButton": "Удалить",
|
||||
"editTitle": "Редактирование конфигурации",
|
||||
"editSubPrefix": "Источник",
|
||||
"editSubSuffix": "· триггеры настраиваются в панели «Триггеры» ниже",
|
||||
"editFieldName": "Имя",
|
||||
"editFieldParent": "Родительская нагрузка",
|
||||
"editFieldOptional": "ОПЦИОНАЛЬНО",
|
||||
"editFieldParentPlaceholder": "UUID нагрузки (пусто для корневой)",
|
||||
"editSourceConfig": "Конфигурация источника",
|
||||
"editConfigYaml": "YAML",
|
||||
"editConfigForm": "ФОРМА",
|
||||
"editConfigJson": "JSON",
|
||||
"advancedJson": "Расширенный JSON",
|
||||
"backToForm": "К форме",
|
||||
"switchToJsonTitle": "Переключиться на сырой JSON-редактор",
|
||||
"switchToFormTitle": "Вернуться к форме",
|
||||
"jsonOk": "JSON ОК",
|
||||
"jsonInvalid": "JSON НЕВЕРНЫЙ",
|
||||
"editComposeProject": "Имя compose-проекта (опционально)",
|
||||
"editComposeProjectPlaceholder": "(по умолчанию — нормализованное имя нагрузки)",
|
||||
"editComposePlaceholder": "services:\n web:\n image: nginx:alpine",
|
||||
"editComposeAria": "Compose YAML",
|
||||
"editComposeHeader": "compose.yaml",
|
||||
"editImageHeader": "image-источник · параметры рантайма",
|
||||
"editImageRef": "Образ (путь в реестре)",
|
||||
"editImageRefPlaceholder": "registry.example.com/owner/app",
|
||||
"editImagePort": "Порт",
|
||||
"editImageHealthcheck": "Путь healthcheck",
|
||||
"editImageDefaultTag": "Тег по умолчанию",
|
||||
"editImageRegistry": "Реестр (для приватных pull-ов)",
|
||||
"editImageRegistryPublic": "(публичный — без авторизации)",
|
||||
"editImageCpu": "Лимит CPU (ядра, 0 = ∞)",
|
||||
"editImageMemory": "Лимит памяти (МБ, 0 = ∞)",
|
||||
"editImageMax": "Макс. инстансов",
|
||||
"editImageFoot": "Переменные окружения и тома живут в своих панелях ниже — сохранение здесь их не затронет.",
|
||||
"editStaticHeader": "static-источник · страницы из репозитория",
|
||||
"editStaticProvider": "Провайдер",
|
||||
"editStaticBaseUrl": "Base URL",
|
||||
"editStaticBaseUrlPlaceholder": "https://git.example.com",
|
||||
"editStaticRepoOwner": "Владелец репозитория",
|
||||
"editStaticRepoName": "Имя репозитория",
|
||||
"editStaticBranch": "Ветка",
|
||||
"editStaticFolder": "Папка (опционально)",
|
||||
"editStaticFolderPlaceholder": "(корень репозитория)",
|
||||
"editStaticToken": "Токен доступа (приватные репозитории)",
|
||||
"editStaticTokenPlaceholder": "(оставьте пустым для публичных репозиториев)",
|
||||
"editStaticMode": "Режим",
|
||||
"editStaticModeStaticDesc": "— раздача файлов через nginx.",
|
||||
"editStaticModeDenoDesc": "— Deno-рантайм с динамической маршрутизацией.",
|
||||
"editStaticRenderMarkdown": "Рендерить markdown",
|
||||
"editStaticRenderMarkdownDesc": "— автоматически отдавать <code>.md</code> как HTML.",
|
||||
"editSourceJsonHeader": "source_config.json",
|
||||
"editSourceJsonAria": "Конфигурация source-плагина (JSON)",
|
||||
"editPublicFaces": "Публичные фронты",
|
||||
"editPublicFacesTag": "JSON МАССИВ",
|
||||
"editPublicFacesHeader": "public_faces.json",
|
||||
"editPublicFacesAria": "Конфигурация публичных фронтов (JSON-массив)",
|
||||
"editCancel": "Отмена",
|
||||
"editSave": "Сохранить",
|
||||
"editSaving": "Сохранение…",
|
||||
"manualDeployTitle": "Ручной деплой",
|
||||
"manualDeployOk": "ОК",
|
||||
"manualDeployDispatched": "Отправлено {reference} как {by}",
|
||||
"manualDeployRefAria": "Референс для деплоя",
|
||||
"manualDeployRefPlaceholder": "референс (тег образа, git sha; пусто — по умолчанию)",
|
||||
"manualDeployButton": "Деплой",
|
||||
"manualDeployDispatching": "Отправка…",
|
||||
"manualDeployHint": "Задайте конкретный тег образа, git sha или ветку, чтобы принудительно деплоить. Оставьте пустым, чтобы source-плагин выбрал референс по умолчанию.",
|
||||
"containersTitle": "Контейнеры",
|
||||
"containersEmpty": "Контейнеров пока нет",
|
||||
"containersCount": "{count} согласовано",
|
||||
"containersEmptyInline": "Контейнеров пока нет — задеплойте, чтобы поднять первый.",
|
||||
"containersColRole": "Роль",
|
||||
"containersColState": "Состояние",
|
||||
"containersColImage": "Образ",
|
||||
"containersColSubdomain": "Поддомен",
|
||||
"containersColLastSeen": "Последний раз виден",
|
||||
"containersColActions": "Действия",
|
||||
"containersLogsAction": "Логи",
|
||||
"chainTitle": "Цепочка",
|
||||
"chainSubFromParent": "продвигается от родителя",
|
||||
"chainSubParentOf": "родитель для",
|
||||
"chainChildSingular": "дочерней нагрузки",
|
||||
"chainChildPlural": "дочерних нагрузок",
|
||||
"chainParentLabel": "Родитель",
|
||||
"chainSelfLabel": "Эта",
|
||||
"chainChildrenLabel": "Дочерние",
|
||||
"chainPromoteButton": "Продвинуть от родителя",
|
||||
"chainPromoting": "Продвижение…",
|
||||
"chainHint": "Задайте <code>parent_workload_id</code> у нагрузки, чтобы построить цепочку. Дочерние image-источники могут одним кликом продвинуть текущий запущенный тег родителя.",
|
||||
"volumesTitle": "Тома",
|
||||
"volumesEmpty": "Нет монтирований",
|
||||
"volumesCountSingular": "{count} монтирование",
|
||||
"volumesCountPlural": "{count} монтирований",
|
||||
"volumesColTarget": "Цель",
|
||||
"volumesColSource": "Источник",
|
||||
"volumesColScope": "Скоуп",
|
||||
"volumesColUpdated": "Обновлено",
|
||||
"volumesColActions": "Действия",
|
||||
"volumeSource": "Источник (хост)",
|
||||
"volumeSourcePlaceholder": "/srv/data/myapp",
|
||||
"volumeTarget": "Цель (контейнер)",
|
||||
"volumeTargetPlaceholder": "/data",
|
||||
"volumeScope": "Скоуп",
|
||||
"volumeAddButton": "Добавить / Заменить",
|
||||
"volumeSaving": "Сохранение…",
|
||||
"volumeHint": "Абсолютные монтирования прокидывают путь хоста в контейнер. Другие скоупы приняты для будущих сценариев; сегодня при деплое применяется только absolute.",
|
||||
"volumeTargetError": "Цель должна быть абсолютным путём в контейнере (например, /data)",
|
||||
"volumeSetFailed": "Не удалось задать том",
|
||||
"volumeDeleteFailed": "Не удалось удалить том",
|
||||
"envTitle": "Окружение",
|
||||
"envEmpty": "Нет переопределений",
|
||||
"envCountSingular": "{count} переопределение",
|
||||
"envCountPlural": "{count} переопределений",
|
||||
"envColKey": "Ключ",
|
||||
"envColValue": "Значение",
|
||||
"envColUpdated": "Обновлено",
|
||||
"envColActions": "Действия",
|
||||
"envEncrypted": "ЗАШИФРОВАНО",
|
||||
"envKey": "Ключ",
|
||||
"envKeyPlaceholder": "DATABASE_URL",
|
||||
"envValue": "Значение",
|
||||
"envValuePlaceholder": "(пусто — снять)",
|
||||
"envEncryptLabel": "Шифровать при хранении",
|
||||
"envAddButton": "Добавить / Заменить",
|
||||
"envSaving": "Сохранение…",
|
||||
"envHint": "Зашифрованные значения после записи доступны только на запись — API скрывает их при чтении. Ротация — установка нового значения.",
|
||||
"envKeyRequired": "Ключ обязателен",
|
||||
"envSetFailed": "Не удалось задать переменную",
|
||||
"envDeleteFailed": "Не удалось удалить переменную",
|
||||
"sourceConfigTitle": "Конфигурация источника",
|
||||
"sourceConfigCopy": "Копировать",
|
||||
"sourceConfigCopied": "Скопировано",
|
||||
"sourceConfigCopyAria": "Копировать конфиг источника",
|
||||
"publicFacesTitle": "Публичные фронты",
|
||||
"publicFacesCopyAria": "Копировать публичные фронты",
|
||||
"deleteConfirmTitle": "Удалить это приложение?",
|
||||
"deleteConfirmMessage": "Сносит все контейнеры и прокси-маршруты, принадлежащие «{name}», затем удаляет запись. Это нельзя отменить.",
|
||||
"deleteConfirmFallbackName": "эта нагрузка",
|
||||
"deleteConfirmYes": "Да, удалить",
|
||||
"deleteConfirmDeleting": "Удаление…",
|
||||
"manualDeploySub": "Обходит настроенные триггеры и отправляет деплой напрямую через source-плагин.",
|
||||
"chainTriggersZero": "без триггеров",
|
||||
"chainTriggersOne": "1 триггер",
|
||||
|
||||
Reference in New Issue
Block a user