⟨/⟩ 40_Arch/Tech/Angular/ADR_UI_014_RuntimeConfig.cs

159 строк · в начало

using System;
using Ban.Sdaid.Notation.Documents;
using Ban.Sdaid.Icd;

namespace Ban.Sdaid.Icd.Arch.Tech.Angular
{
    /// <summary>
    /// ADR-UI-014: Рантайм-конфигурация. Адрес API и прочие параметры среды приложение
    /// узнаёт при старте из `config.json` — одна сборка работает на всех средах.
    /// </summary>
    public class ADR_UI_014_RuntimeConfig : IAdrDocument, IFolder<ArchAngularFolder>
    {
        public string Name => "ADR-UI-014. Рантайм-конфигурация";

        public string Description =>
            @"Фронт должен знать адрес API (и другие параметры среды) в момент старта. Параметры различаются
по средам (local/dev/stage/prod), но **сборка одна**. Фиксируем: параметры грузятся из `config.json` в
рантайме (не build-time `environment.ts`) через `provideAppInitializer` до bootstrap; `apiBaseUrl` берётся
только из `RuntimeConfigService`; деплой подменяет только `config.json`.";

        public string Version => "1.0";
        public string Status => "accepted";
        public string[] Comments => new[]
        {
            "2026-08-13. Принято перед разворачиванием на стенде: фронт и API на разных origin, адрес API — из рантайм-конфига.",
        };

        public Type? Supersedes => null;

        public static string S1_Context = """
            ## Контекст и постановка задачи

            Приложению нужен адрес API (и потенциально другие параметры) на старте. Эти параметры
            **различаются между средами** (локальная разработка, dev-стенд, stage, прод), но **артефакт
            сборки должен быть один** — иначе выпуск в каждую среду требует отдельного билда, что усложняет
            релиз и повышает риск расхождений.

            Стандартный шаблон Angular (`environments/environment.ts`) не подходит: он работает на этапе
            **сборки**, а не старта. Нужен способ подменять конфиг у уже собранного приложения.

            У нас фронт и API на стенде — на **разных origin**, поэтому `apiBaseUrl` — абсолютный, а бэк
            держит CORS-allowlist по среде (см. связанный backend-ADR).
            """;

        public static string S2_DecisionDrivers = """
            ## Драйверы решения

            1. **Один билд на все среды** — конфиг подменяется без пересборки.
            2. **Конфиг гарантированно готов к первому запросу** — никакой код не обратится к API раньше,
               чем конфиг загружен.
            3. **Прозрачный доступ** — `apiBaseUrl` берётся инъекцией очевидного сервиса, без чтения
               `window` и глобальных объектов.
            4. **Стандартный механизм Angular** — без хаков в `index.html`.
            """;

        public static string S3_ConsideredOptions = """
            ## Рассмотренные варианты

            - **A. `config.json` + `provideAppInitializer` + `fetch`** — Angular ждёт резолва инициализатора
              до bootstrap; конфиг кладётся в сервис-сигнал.
            - **B. `<script src="config.js">` в `index.html`** — синхронно выставляет глобальный объект до
              bootstrap. Без асинхронного ожидания, но завязан на `window` и тег `<script>`.
            - **C. `environment.ts`** — build-time; требует пересборки на каждую среду. Отклонено.
            """;

        public static string S4_DecisionOutcome = """
            ## Решение

            Выбран **Вариант A**: `config.json` грузится через `provideAppInitializer` и кладётся в
            `RuntimeConfigService`.

            ### Контракт `config.json`

            Файл — в `src-ui/public/config.json` (Angular кладёт содержимое `public/` в корень dist).
            Это **рантайм-артефакт**: на каждой среде заменяется своей версией средствами деплоя.

            ```json
            { "apiBaseUrl": "https://sok-api.dev.example" }
            ```

            | Ключ | Тип | Назначение |
            |---|---|---|
            | `apiBaseUrl` | string | Origin API — без завершающего слэша и без `/api`. |

            Расширение состава ключей — дополнением этого раздела, без нового ADR.

            ### `RuntimeConfigService`

            - `providedIn: 'root'`, хранит конфиг в signal;
            - `load()` — `fetch('config.json', { cache: 'no-store' })`, вызывается из `provideAppInitializer`;
            - `apiBaseUrl` — геттер; при обращении до инициализации бросает явную ошибку (не «тихий» undefined).

            ```ts
            provideAppInitializer(() => inject(RuntimeConfigService).load()),
            ```

            ### Связка с генерированным API-клиентом

            Базовый URL сгенерированного клиента (`@api/icd`) задаётся токеном `BASE_PATH` — через фабрику
            из `RuntimeConfigService` (ADR-UI-012):

            ```ts
            { provide: BASE_PATH, useFactory: () => inject(RuntimeConfigService).apiBaseUrl },
            ```

            Токен резолвится при первом создании api-сервиса (открытие страницы) — уже после инициализатора,
            конфиг гарантированно загружен.

            ### Правила

            1. **`apiBaseUrl` — только из `RuntimeConfigService`.** Никаких `window.*`, прямого чтения
               `config.json`, импорта из `environment.ts`.
            2. **`config.json` не кэшируется** — `no-store` в `fetch` и на отдаче с сервера; иначе смена
               конфига потребует жёсткого обновления у каждого пользователя.
            3. **В репозитории лежит `config.json` с локальными значениями** (`http://localhost:5080`);
               реальные значения сред — в артефактах деплоя (генерятся `devops/configs-generator`).
            4. **Секретов в `config.json` нет** — значения видны в network tab; только публичные параметры.
            """;

        public static string S5_PositiveConsequences = """
            ## Положительные следствия

            - Один билд → все среды; деплой меняет только `config.json`.
            - К первому обращению к API конфиг уже загружен — нет гонок.
            - Расширение конфига — поле в интерфейсе и в `config.json`, без пересборки потребителей.
            """;

        public static string S6_NegativeConsequences = """
            ## Отрицательные следствия и компромиссы

            - Старт добавляет один HTTP-запрос (сотни байт). На медленной сети заметно; при необходимости —
              переход на синхронный `<script>` (вариант B).
            - Значения конфига видны в браузере — секреты туда класть нельзя.
            """;

        public static string S7_Validation = """
            ## Проверка

            - Подмена `config.json` в собранном dist меняет адрес API без пересборки.
            - Обращение к `apiBaseUrl` до инициализации даёт явную ошибку, не `undefined`.
            - В коде нет обращений к API мимо `RuntimeConfigService` / токена `BASE_PATH`.
            """;

        public static string S8_OpenQuestions = """
            ## Открытые вопросы / отложено

            - **Доп. ключи** (`appVersion`, флаги фич) — по мере необходимости, дополнением этого ADR.
            - **Отдача `config.json` без кэша на CDN/прокси** — настройка инфраструктуры; `no-store` обязателен.
            """;

        public static string S99_Related = $"""
            ## Связанные артефакты

            - {nameof(Ban.Sdaid.Icd.Arch.Tech.CSharp.ADR_SRV_004_AppConfigAndCors)} — конфиг бэка и CORS-allowlist по среде.
            - {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_012_ApiAndModels)} — регистрация api-клиента и токен базового URL `BASE_PATH`.
            """;
    }
}