diff --git a/.dockerignore b/.dockerignore index 287d583..fad0483 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,3 +7,6 @@ docs .gitea docker-compose*.yml deploy +web/node_modules +web/.svelte-kit +web/build diff --git a/Dockerfile b/Dockerfile index 0d803e0..9b362bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,35 @@ # syntax=docker/dockerfile:1 +# --- Web UI (SvelteKit static) — тот же origin, что и API (порт шлюза) +FROM node:22-bookworm AS webui +WORKDIR /web +COPY web/package.json web/package-lock.json ./ +RUN npm ci +COPY web/ ./ +# Пустой URL: fetch идёт на тот же хост/порт, что и панель +ENV PUBLIC_TELEMT_GATEWAY_URL= +RUN npm run build + FROM golang:1.22-bookworm AS build WORKDIR /src COPY go.mod ./ COPY cmd/ ./cmd/ COPY internal/ ./internal/ +COPY --from=webui /web/build/ ./internal/webui/static/ RUN go mod tidy && go mod download RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/gateway ./cmd/gateway # Alpine: non-root + wget for HEALTHCHECK (distroless has no shell/wget). FROM alpine:3.19 RUN apk add --no-cache ca-certificates wget \ - && addgroup -S gateway -g 65532 \ - && adduser -S -u 65532 -G gateway gateway \ - && mkdir -p /var/lib/telemt-gateway \ - && chown gateway:gateway /var/lib/telemt-gateway + && addgroup -S gateway -g 65532 \ + && adduser -S -u 65532 -G gateway gateway \ + && mkdir -p /var/lib/telemt-gateway \ + && chown gateway:gateway /var/lib/telemt-gateway COPY --from=build /out/gateway /gateway USER gateway:gateway EXPOSE 8080 ENV CONFIG_PATH=/etc/telemt-gateway/config.yaml HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget -q -O- http://127.0.0.1:8080/health >/dev/null || exit 1 + CMD wget -q -O- http://127.0.0.1:8080/health >/dev/null || exit 1 ENTRYPOINT ["/gateway"] diff --git a/README.md b/README.md index 7cfe9b7..901355d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # telemt-api -HTTP‑шлюз на Go для [Telemt Control API](docs/API.md): один порт, **белый список IP (CIDR)**, маршруты вида `/api/{alias}/…` → `{base_url}/v1/…`, агрегация нескольких инстансов — [`/api/agg/…`](docs/AGGREGATE.md), метрики Prometheus на `/metrics`. +HTTP‑шлюз на Go для [Telemt Control API](docs/API.md): один порт, **белый список IP (CIDR)**, маршруты вида `/api/{alias}/…` → `{base_url}/v1/…`, агрегация нескольких инстансов — [`/api/agg/…`](docs/AGGREGATE.md), метрики Prometheus на `/metrics`. **Web UI** (SvelteKit) встроен в тот же процесс/образ: статика на `/`, API на `/api/…` и `/health`. ## Быстрый старт (Linux) Предполагается установлены Docker и Docker Compose v2. -**Рекомендуется** брать уже собранный образ из Container Registry Gitea (после каждого push в репозиторий CI обновляет теги, в том числе `latest`): +**Рекомендуется** брать уже собранный образ из Container Registry Gitea (после каждого push CI обновляет теги, в том числе `latest`). В актуальном образе вместе с шлюзом уже **встроена панель** на `http://<хост>:8080/`: ```bash # при необходимости (закрытый registry): логин Gitea + PAT с read:package @@ -30,25 +30,25 @@ docker run -d --name telemt-gateway \ curl -sS http://127.0.0.1:8080/health curl -sS http://127.0.0.1:8080/api/main_srv/health +# панель в браузере: http://127.0.0.1:8080/ ``` Обновление образа: `docker pull git.shts.su/denozord/telemt-api:latest` и пересоздайте контейнер (`docker rm -f telemt-gateway` и снова `docker run …`). ### Compose -Тот же образ шлюза подтягивается из registry (без локальной сборки). Сервис **web** (панель в браузере) собирается из каталога `web/` при первом запуске: +Один сервис **gateway** — в образе уже есть и API, и статика панели (см. [Dockerfile](Dockerfile)). ```bash git clone && cd telemt-api docker compose pull -docker compose up -d --build +docker compose up -d docker compose logs -f gateway ``` -- Шлюз: `http://127.0.0.1:8080` (проверка: `curl -sS http://127.0.0.1:8080/health`). -- Web UI: `http://127.0.0.1:4173` — в [config.compose.yaml](config.compose.yaml) заданы `cors_allowed_origins` под этот origin; `PUBLIC_TELEMT_GATEWAY_URL` при сборке образа UI указывает на шлюз на хосте (`http://127.0.0.1:8080`). +- Всё на **одном порту**: `http://127.0.0.1:8080/` — Web UI, `http://127.0.0.1:8080/api/…` — шлюз, `http://127.0.0.1:8080/health` — проверка живости. -Подробнее по панели: [web/README.md](web/README.md). +Подробнее по фронту и dev-режиму: [web/README.md](web/README.md). Полный сценарий Docker (CLI, compose, сборка, CI): [docs/GATEWAY_RUN.md](docs/GATEWAY_RUN.md). ### Web UI (локально без Docker) @@ -71,7 +71,7 @@ cors_allowed_origins: ### Локальная сборка образа -Если нужен образ из исходников на этой машине: `docker build -t telemt-api-gateway:local .` и в `docker run` укажите тег `telemt-api-gateway:local`. Подробнее — [docs/GATEWAY_RUN.md](docs/GATEWAY_RUN.md). +`docker build -t telemt-api-gateway:local .` — в образ попадают Web UI и бинарь шлюза (см. [Dockerfile](Dockerfile)). В `docker run` укажите этот тег. Подробнее — [docs/GATEWAY_RUN.md](docs/GATEWAY_RUN.md). ## Документация @@ -81,7 +81,7 @@ cors_allowed_origins: | **[docs/API.md](docs/API.md)** | Контракт Telemt Control API (`/v1/…`) | | **[docs/AGGREGATE.md](docs/AGGREGATE.md)** | Агрегирующие эндпоинты шлюза (`/api/agg/…`), CORS, кэш | | **[docs/AGGREGATE_OPENAPI.yaml](docs/AGGREGATE_OPENAPI.yaml)** | OpenAPI 3 черновик для `/api/agg/*` (генерация типов для UI) | -| **[web/README.md](web/README.md)** | Web UI (SvelteKit + shadcn-svelte): установка, `PUBLIC_TELEMT_GATEWAY_URL`, Docker | +| **[web/README.md](web/README.md)** | Web UI (SvelteKit): разработка с Vite, `PUBLIC_TELEMT_GATEWAY_URL`, встраивание в образ шлюза | | **[docs/GEOIP.md](docs/GEOIP.md)** | GeoLite2 City (страна/город) и опционально ASN (номер AS, организация) для IP в `unique-ips` | ## Сборка и тесты без Docker diff --git a/config.compose.yaml b/config.compose.yaml index d9b2550..e96fa3f 100644 --- a/config.compose.yaml +++ b/config.compose.yaml @@ -4,10 +4,7 @@ listen: ":8080" allow_all: true whitelist_cidrs: [] trusted_proxies: [] -# UI в Docker (сервис web): браузер на другом origin — нужен CORS -cors_allowed_origins: - - "http://127.0.0.1:4173" - - "http://localhost:4173" +# UI отдаётся с того же порта, что и API (образ шлюза) — отдельный CORS для панели не нужен. servers: - alias: main_srv base_url: http://host.docker.internal:9091 diff --git a/config.example.yaml b/config.example.yaml index 4629013..322d247 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -2,6 +2,9 @@ # # Semantics: client calls GET /api/{alias}/health # forwarded to GET {base_url}/v1/health +# +# Web UI (SvelteKit) в образе Docker встроен в тот же процесс: GET / — панель, +# /api/… — API. CORS для панели на том же origin не нужен. listen: ":8080" @@ -23,7 +26,8 @@ whitelist_cidrs: # - "10.0.0.0/8" trusted_proxies: [] -# SPA на другом origin (preflight OPTIONS + Access-Control-Allow-Origin): +# CORS только если фронт на другом origin (например Vite :5173 при разработке). +# В production за nginx на одном хосте с шлюзом обычно не требуется. # cors_allowed_origins: # - "http://localhost:5173" # # - "*" diff --git a/docker-compose.yml b/docker-compose.yml index f7879ef..559d4a6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,7 @@ services: - web: - build: - context: ./web - dockerfile: Dockerfile - args: - # URL шлюза в браузере (встраивается при сборке) - PUBLIC_TELEMT_GATEWAY_URL: http://127.0.0.1:8080 - ports: - - "4173:4173" gateway: image: git.shts.su/denozord/telemt-api:latest - # Локальная сборка вместо pull: укажите build: . и image: telemt-api-gateway:local + # Локальная сборка с Web UI внутри образа: build: . и image: telemt-api-gateway:local ports: - "8080:8080" volumes: diff --git a/docs/GATEWAY_RUN.md b/docs/GATEWAY_RUN.md index 64a2b31..803296a 100644 --- a/docs/GATEWAY_RUN.md +++ b/docs/GATEWAY_RUN.md @@ -18,6 +18,7 @@ Шлюз — это один HTTP‑вход для нескольких экземпляров [Telemt Control API](API.md): +- **Web UI** (в образе Docker): статика панели на **`GET /`** (и клиентские маршруты SPA), агрегаты и прокси на **`/api/…`**. Тот же порт, что и у API (например `8080`). Исходники UI — каталог [web/](../web/README.md), сборка встроена в [Dockerfile](../Dockerfile) (стадия Node + `embed` в Go). - **Белый список IP** (CIDR): кто может обращаться к шлюзу (кроме `GET /health`, см. ниже). - **Маршрутизация по alias**: клиент вызывает `GET /api/{alias}/health`, шлюз проксирует на `{base_url}/v1/health` у соответствующего сервера. - **Метрики Prometheus**: `GET /metrics` (под тем же правилом whitelist, что и API). @@ -28,7 +29,8 @@ ## Требования - Установленные [Docker](https://docs.docker.com/get-docker/) и при необходимости [Docker Compose](https://docs.docker.com/compose/) v2. -- Для локальной сборки из исходников: [Go 1.22+](https://go.dev/dl/) (опционально, если не используете только готовый образ из registry). +- Для локальной сборки **Docker-образа** из репозитория: Docker сам подтянет [Node](https://nodejs.org/) на стадии сборки фронта и [Go 1.22+](https://go.dev/dl/) на стадии компиляции (см. [Dockerfile](../Dockerfile)). +- Для `go test ./...` без Docker на машине нужен только Go 1.22+. ## Минимальная конфигурация @@ -106,7 +108,7 @@ docker login git.shts.su docker build -t telemt-api-gateway:local . ``` -В командах `docker run` ниже вместо имени из registry подставьте `telemt-api-gateway:local`. +Сборка **многостадийная**: сначала `npm ci` + `npm run build` в каталоге `web/` (в бандл вшивается пустой `PUBLIC_TELEMT_GATEWAY_URL`, запросы API с того же origin), затем компиляция Go со встраиванием `web/build` через `embed`. В командах `docker run` ниже вместо имени из registry подставьте `telemt-api-gateway:local`. ## Запуск через Docker CLI @@ -125,9 +127,12 @@ docker run -d --name telemt-gateway \ ```bash curl -sS -i http://127.0.0.1:8080/health curl -sS -i http://127.0.0.1:8080/api/main_srv/health +curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/ ``` -Второй запрос проксируется на `{base_url}/v1/health` для alias `main_srv`. +Второй запрос проксируется на `{base_url}/v1/health` для alias `main_srv`. Третий — HTML панели (в образах, собранных с Web UI; ожидайте `200`). + +Панель в браузере: `http://127.0.0.1:8080/` (при `allow_all: false` ваш IP должен быть в `whitelist_cidrs`, иначе для `/` будет `403`, как и для API). Остановка и удаление: @@ -138,7 +143,7 @@ docker rm telemt-gateway ## Запуск через Docker Compose -В репозитории есть [docker-compose.yml](../docker-compose.yml) и пример [config.compose.yaml](../config.compose.yaml) с `allow_all: true` и `base_url: http://host.docker.internal:9091` (Telemt на хосте; на Docker Desktop для Linux это обычно работает из коробки). Образ по умолчанию **скачивается** из registry, локальная сборка не требуется. +В репозитории есть [docker-compose.yml](../docker-compose.yml) (один сервис **gateway**) и пример [config.compose.yaml](../config.compose.yaml) с `allow_all: true` и `base_url: http://host.docker.internal:9091` (Telemt на хосте; на Docker Desktop для Linux это обычно работает из коробки). Образ по умолчанию **скачивается** из registry, локальная сборка не требуется. UI доступен на том же порту, что и шлюз: `http://127.0.0.1:8080/`. ```bash docker compose pull @@ -159,6 +164,7 @@ docker compose down | Сценарий | Ожидание | |----------|----------| | `GET /health` | `200`, JSON `{"status":"ok"}` | +| `GET /` (образ с Web UI) | `200`, HTML панели | | Разрешённый IP, корректный alias | ответ бэкенда (например `200` для `/v1/health`) | | IP не в whitelist | `403`, JSON с `code: forbidden` | | Неизвестный alias | `404`, JSON с `code: not_found` | @@ -167,9 +173,9 @@ docker compose down ## Обновление и CI/CD -- **Образ**: подтяните свежий тег (`docker pull git.shts.su/denozord/telemt-api:latest` или `docker compose pull`), пересоздайте контейнер (`docker compose up -d` или новый `docker run` с тем же volume конфига). Локальная пересборка нужна только если вы меняете Dockerfile/код и не пользуетесь CI. +- **Образ**: подтяните свежий тег (`docker pull git.shts.su/denozord/telemt-api:latest` или `docker compose pull`), пересоздайте контейнер (`docker compose up -d` или новый `docker run` с тем же volume конфига). Локальная пересборка нужна только если вы меняете Dockerfile/код и не пользуетесь CI. Образы, собранные **до** добавления стадии `web/` в Dockerfile, могут отдавать на `/` только заглушку — нужен образ из актуального CI или локальный `docker build`. - **Конфиг**: отредактируйте файл на хосте и перезапустите контейнер (шлюз не перечитывает конфиг на лету). -- **Gitea Actions**: workflow [.gitea/workflows/docker.yaml](../.gitea/workflows/docker.yaml) сначала выполняет `go mod tidy && go test ./...`, затем собирает образ через Buildx и пушит в Container Registry Gitea. +- **Gitea Actions**: workflow [.gitea/workflows/docker.yaml](../.gitea/workflows/docker.yaml) сначала выполняет `go mod tidy && go test ./...`, затем собирает образ через Buildx по [Dockerfile](../Dockerfile) (стадии Node для `web/` и Go) и пушит в Container Registry Gitea. - В репозитории должен быть secret **`ACTIONS_PAT`** — personal access token пользователя с правом **`write:package`** (и при необходимости `read:package`), как для обычного `docker login` к registry. - Логин в registry: пользователь **`gitea.actor`** (кто запустил workflow), пароль — этот PAT. diff --git a/internal/server/gateway.go b/internal/server/gateway.go index 502f78b..c4ea028 100644 --- a/internal/server/gateway.go +++ b/internal/server/gateway.go @@ -17,6 +17,7 @@ import ( "github.com/telemt/telemt-api/internal/config" "github.com/telemt/telemt-api/internal/geoip" "github.com/telemt/telemt-api/internal/proxy" + "github.com/telemt/telemt-api/internal/webui" ) // Gateway serves health, metrics, and proxied API routes. @@ -29,6 +30,7 @@ type Gateway struct { transport *http.Transport promHandler http.Handler corsAllowed []string + webUI http.Handler } // NewGateway builds handlers and reverse proxies from parsed config. @@ -75,6 +77,7 @@ func NewGateway(p *config.Parsed, log *slog.Logger, geo *geoip.Service) (*Gatewa } g.corsAllowed = append([]string(nil), p.Config.CorsAllowedOrigins...) g.agg = aggregate.NewHandler(p, &http.Client{Transport: t}, geo, aggCacheTTL) + g.webUI = webui.Handler() return g, nil } @@ -231,7 +234,7 @@ func (g *Gateway) serve(w http.ResponseWriter, r *http.Request) { return } if !strings.HasPrefix(r.URL.Path, prefix) { - http.NotFound(w, r) + g.webUI.ServeHTTP(w, r) return } trim := strings.TrimPrefix(r.URL.Path, prefix) diff --git a/internal/webui/embed.go b/internal/webui/embed.go new file mode 100644 index 0000000..258c7b8 --- /dev/null +++ b/internal/webui/embed.go @@ -0,0 +1,6 @@ +package webui + +import "embed" + +//go:embed all:static +var static embed.FS diff --git a/internal/webui/handler.go b/internal/webui/handler.go new file mode 100644 index 0000000..3160b00 --- /dev/null +++ b/internal/webui/handler.go @@ -0,0 +1,68 @@ +package webui + +import ( + "io/fs" + "mime" + "net/http" + "path" + "strconv" + "strings" +) + +// Handler отдаёт статику SvelteKit (embed) и index.html для клиентских маршрутов SPA. +func Handler() http.Handler { + root, err := fs.Sub(static, "static") + if err != nil { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.NotFound(w, r) + }) + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodHead { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + name := strings.TrimPrefix(path.Clean(r.URL.Path), "/") + if name == "." || name == "" { + name = "index.html" + } + + b, err := fs.ReadFile(root, name) + if err != nil { + if path.Ext(name) != "" { + http.NotFound(w, r) + return + } + b, err = fs.ReadFile(root, "index.html") + if err != nil { + http.NotFound(w, r) + return + } + name = "index.html" + } + + ct := mime.TypeByExtension(path.Ext(name)) + if ct == "" { + ct = "application/octet-stream" + } + if strings.HasPrefix(ct, "text/") && !strings.Contains(ct, "charset") { + ct = ct + "; charset=utf-8" + } + w.Header().Set("Content-Type", ct) + if name == "index.html" { + w.Header().Set("Cache-Control", "no-cache") + } else { + w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + } + + if r.Method == http.MethodHead { + w.Header().Set("Content-Length", strconv.Itoa(len(b))) + w.WriteHeader(http.StatusOK) + return + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write(b) + }) +} diff --git a/internal/webui/handler_test.go b/internal/webui/handler_test.go new file mode 100644 index 0000000..53eb513 --- /dev/null +++ b/internal/webui/handler_test.go @@ -0,0 +1,28 @@ +package webui + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestHandler_index(t *testing.T) { + h := Handler() + rr := httptest.NewRecorder() + h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/", nil)) + if rr.Code != http.StatusOK { + t.Fatalf("GET /: %d", rr.Code) + } + if got := rr.Header().Get("Content-Type"); got == "" { + t.Fatal("missing Content-Type") + } +} + +func TestHandler_spaFallback(t *testing.T) { + h := Handler() + rr := httptest.NewRecorder() + h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/servers/foo", nil)) + if rr.Code != http.StatusOK { + t.Fatalf("GET /servers/foo: %d", rr.Code) + } +} diff --git a/internal/webui/static/index.html b/internal/webui/static/index.html new file mode 100644 index 0000000..f4ae627 --- /dev/null +++ b/internal/webui/static/index.html @@ -0,0 +1,13 @@ + + + + + Telemt Panel + + +

+ Заглушка: UI не собран. Соберите фронт (cd web && npm run build) перед сборкой образа шлюза, либо + используйте Dockerfile в корне репозитория. +

+ + diff --git a/web/.env.example b/web/.env.example index 0ac7de2..7e9ff49 100644 --- a/web/.env.example +++ b/web/.env.example @@ -1,2 +1,3 @@ -# Базовый URL шлюза telemt-api (без завершающего слэша) -PUBLIC_TELEMT_GATEWAY_URL=http://127.0.0.1:8080 +# Пусто = запросы к API на том же origin, что и панель (образ Docker со встроенным UI). +# Для dev (Vite :5173, шлюз :8080): http://127.0.0.1:8080 +PUBLIC_TELEMT_GATEWAY_URL= diff --git a/web/Dockerfile b/web/Dockerfile deleted file mode 100644 index 960333e..0000000 --- a/web/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -# Статическая сборка SvelteKit и раздача через serve -FROM node:22-alpine AS build -WORKDIR /app -ARG PUBLIC_TELEMT_GATEWAY_URL=http://127.0.0.1:8080 -ENV PUBLIC_TELEMT_GATEWAY_URL=$PUBLIC_TELEMT_GATEWAY_URL -COPY package.json package-lock.json ./ -RUN npm ci -COPY . . -RUN npm run build - -FROM node:22-alpine -RUN npm install -g serve@14 -WORKDIR /srv -COPY --from=build /app/build ./build -EXPOSE 4173 -CMD ["serve", "-s", "build", "-l", "4173"] diff --git a/web/README.md b/web/README.md index b863184..5eaca71 100644 --- a/web/README.md +++ b/web/README.md @@ -1,16 +1,17 @@ # Telemt Panel (Web UI) -SvelteKit + shadcn-svelte: обзор флота (`/api/agg/*`), пользователи и IP, панель по каждой ноде через прокси шлюза (`/api/{alias}/…`). +SvelteKit + shadcn-svelte. В **production** статика собирается и **встраивается в образ шлюза** ([Dockerfile](../Dockerfile) в корне репозитория): панель и API на **одном порту** (например `http://127.0.0.1:8080/` — UI, `/api/…` — шлюз). -## Переменные окружения +## Переменная `PUBLIC_TELEMT_GATEWAY_URL` -| Переменная | Описание | +| Значение | Когда | | --- | --- | -| `PUBLIC_TELEMT_GATEWAY_URL` | Базовый URL шлюза **telemt-api** без завершающего `/` (например `http://127.0.0.1:8080`). Встраивается в клиент при сборке. | +| *(пусто)* | Тот же хост и порт, что у страницы (Docker-образ шлюза, `npm run build` в Dockerfile с `ENV PUBLIC_TELEMT_GATEWAY_URL=`) | +| `http://127.0.0.1:8080` | Локальная разработка: Vite на `:5173`, шлюз на `:8080` | -Скопируйте [.env.example](.env.example) в `.env` и при необходимости измените URL. +Скопируйте [.env.example](.env.example) в `.env` и при необходимости задайте URL. -## Разработка +## Разработка (Vite отдельно от Go) ```powershell cd web @@ -18,39 +19,30 @@ npm install npm run dev ``` -Откройте в браузере адрес Vite (по умолчанию `http://localhost:5173`). В конфиге шлюза укажите CORS, например: +Откройте `http://localhost:5173`. В конфиге шлюза включите [CORS](../docs/AGGREGATE.md) для этого origin: ```yaml cors_allowed_origins: - "http://localhost:5173" ``` -## Сборка +## Сборка только фронта ```powershell npm run build -npm run preview # проверка статики ``` -## Типы из OpenAPI агрегатов +Артефакт — каталог `build/`, при сборке **Docker-образа шлюза** он копируется в `internal/webui/static/` и попадает в бинарник через `embed`. -После изменения [../docs/AGGREGATE_OPENAPI.yaml](../docs/AGGREGATE_OPENAPI.yaml): +## Типы из OpenAPI агрегатов ```powershell npm run gen:api ``` -## Docker - -Сборка образа из каталога `web/`: - -```powershell -docker build -t telemt-web:local --build-arg PUBLIC_TELEMT_GATEWAY_URL=http://127.0.0.1:8080 . -``` - -В [docker-compose.yml](../docker-compose.yml) сервис `web` отдаёт UI на порту **4173**. URL шлюза в образе задаётся **build-arg** (см. compose). Браузер на хосте обращается к шлюзу по `http://127.0.0.1:8080` — при другом адресе пересоберите образ с нужным `PUBLIC_TELEMT_GATEWAY_URL`. +Источник: [../docs/AGGREGATE_OPENAPI.yaml](../docs/AGGREGATE_OPENAPI.yaml). ## Ограничения - Секреты upstream к Telemt задаются на шлюзе (`authorization_env`), не в браузере. -- Mutating CORS: шлюз должен разрешать `POST`, `PATCH`, `DELETE` в preflight (в текущей версии репозитория заголовок `Access-Control-Allow-Methods` это учитывает). +- Для мутаций из другого origin шлюз отдаёт нужные заголовки CORS (в т.ч. методы `POST`, `PATCH`, `DELETE`).