diff --git a/HISTORY/2026-08-16-session-log.md b/HISTORY/2026-08-16-session-log.md index 1cfce62..cf77ea5 100644 --- a/HISTORY/2026-08-16-session-log.md +++ b/HISTORY/2026-08-16-session-log.md @@ -1178,3 +1178,28 @@ paho connect() rc=0 даже при CONNACK≠0 (on_connect обязателен burst считает по фактическому sent, large-payload без API-верификации. - Repo-память: /memories/repo/iot.md (факты платформы, правила деплоя). - ОТЛОЖЕНО: tenant-whitelist (#7), DLQ в shared-sqs, soak 24ч. + +--- + +## 33. Лендинг-инструкция в дизайне Nubes (v0.1.8) (19:00 GMT+03) + +- Задание: страницы-инструкции «что это, зачем, как пользоваться» в дизайне + из ~/nubes/design (лого + favicon), образец — https://sqs.containerk8s.dev.nubes.ru/. +- Скопирована структура и CSS shared-sqs-лендинга (topbar/hero/badge/card/ + steps/kv/hint-box/footer, цвета дизайн-системы: #2563eb, #f3f4f6, #d1d5db). +- Новое: `internal/service/api/ui/landing.html` (лендинг "/"), `ui/static/ + logo.svg` + `favicon.svg` (из ~/nubes/design), `landing_embed.go` + (embed + ServeLanding + StaticHandler), маршруты "/" и "/static/" в router. +- Содержание лендинга: что это (конвейер устройство→MQTT→SQS→PG→API), + 3 шага (регистрация устройства с curl-примерами; подключение MQTT с paho- + примером; чтение телеметрии), карточка «Подключение» (endpoint, протокол, + auth MQTT/REST, топик, консоль, health), секция «MQTT endpoint (exqx)» + (объяснение 400 и ограничений: 250KB, wss ~150с). +- favicon добавлен в /console и /iot-admin. +- ⚠ exqx.containerk8s.dev.nubes.ru — EMQX без возможности отдавать HTML + (нет статики в OSS, dashboard отключён, один EXPOSE 8083 занят WS) → + инструкция по MQTT-endpoint живёт на iot-лендинге, секция «MQTT endpoint». +- Грабли: embed FS требует StripPrefix("/static/"); версия в чипе + подставляется через {{VERSION}} (без лишней "v"). +- v0.1.8 (digest db648725dfe6) задеплоен; проверка: "/" 200, favicon/logo + 200 (image/svg+xml), chip v0.1.8. diff --git a/Makefile b/Makefile index 6623203..3729c49 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Makefile — монолит iot-service (образ naeel/iot-service). # Старые k8s-цели — в legacy/Makefile.old. -VERSION ?= v0.1.6 +VERSION ?= v0.1.8 IMAGE ?= naeel/iot-service LDFLAGS = -X main.version=$(VERSION) diff --git a/internal/service/api/landing_embed.go b/internal/service/api/landing_embed.go new file mode 100644 index 0000000..ff9afb8 --- /dev/null +++ b/internal/service/api/landing_embed.go @@ -0,0 +1,40 @@ +// landing_embed.go — встраивает лендинг-инструкцию и статику (лого, favicon). +package api + +import ( + "embed" + "io/fs" + "net/http" + "strings" +) + +// landingHTML — лендинг "/" с инструкцией (дизайн shared-sqs). +// +//go:embed ui/landing.html +var landingHTML []byte + +// staticFS — логотип и favicon (дизайн Nubes, как на shared-sqs). +// +//go:embed ui/static +var staticFS embed.FS + +// versionPlaceholder — заменяется на версию при отдаче страницы. +const versionPlaceholder = "{{VERSION}}" + +// ServeLanding отдаёт лендинг "/" с подставленной версией. +func ServeLanding(w http.ResponseWriter, r *http.Request, version string) { + body := strings.ReplaceAll(string(landingHTML), versionPlaceholder, version) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Cache-Control", "no-cache, must-revalidate") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(body)) +} + +// StaticHandler раздаёт /static/* (logo.svg, favicon.svg) из embed. +func StaticHandler() http.Handler { + sub, err := fs.Sub(staticFS, "ui/static") + if err != nil { + panic(err) + } + return http.FileServer(http.FS(sub)) +} diff --git a/internal/service/api/router.go b/internal/service/api/router.go index 52a83c5..7775a45 100644 --- a/internal/service/api/router.go +++ b/internal/service/api/router.go @@ -33,6 +33,12 @@ func NewRouter(h *handler.Handler, log *slog.Logger, authTestMode bool, jwtHMACS r.HandleFunc("/health", healthHandler(version)).Methods(http.MethodGet) r.HandleFunc("/healthz", healthHandler(version)).Methods(http.MethodGet) + // Лендинг "/" — инструкция (дизайн Nubes, как у shared-sqs) + статика. + r.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + ServeLanding(w, req, version) + }).Methods(http.MethodGet) + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", StaticHandler())) + // UI — публичные страницы (без данных). r.HandleFunc("/console", ServeIoTConsole).Methods(http.MethodGet) r.HandleFunc("/iot-admin", ServeIoTAdmin).Methods(http.MethodGet) diff --git a/internal/service/api/ui/iot-admin.html b/internal/service/api/ui/iot-admin.html index 2195a13..68d503a 100644 --- a/internal/service/api/ui/iot-admin.html +++ b/internal/service/api/ui/iot-admin.html @@ -10,6 +10,7 @@
Приём и хранение телеметрии устройств: MQTT → шина SQS → + PostgreSQL → REST API и консоль. Устройства изолированы по namespace, + каждому namespace — отдельная база данных.
+Платформа принимает телеметрию от устройств по протоколу MQTT + (WebSocket с TLS) и доставляет её в базу данных по цепочке:
+
+ устройство → wss://…/mqtt (EMQX) → очередь
+ iot-telemetry (shared-sqs) → потребитель → PostgreSQL →
+ REST API / консоль.
+
+ Устройства регистрируются через REST API и получают собственный
+ пароль. Каждое устройство публикует только в свой топик
+ {namespace}/telemetry/{device_id} — чужие топики
+ отклоняются брокером.
+
Устройство создаётся через REST API с Bearer-JWT (структура
+ sub + exp). В ответе — mqtt_username;
+ пароль возвращается только запросом GET по имени устройства.
TOKEN="…jwt…"curl -X POST -H "Authorization: Bearer $TOKEN" \
+ -H "Content-Type: application/json" \
+ -d '{"name":"dev1","device_id":"dev-001"}' \
+ https://iot.containerk8s.dev.nubes.ru/v1/namespaces/test/iot/devicescurl -H "Authorization: Bearer $TOKEN" \
+ https://iot.containerk8s.dev.nubes.ru/v1/namespaces/test/iot/devices/dev1
+ Endpoint wss://exqx.containerk8s.dev.nubes.ru/mqtt,
+ подпротокол WebSocket — mqtt; username
+ {namespace}_{device_id}, пароль из шага 1; публикация в
+ топик {namespace}/telemetry/{device_id}.
import paho.mqtt.client as mqttc = mqtt.Client(transport="websockets")c.ws_set_options(path="/mqtt")c.username_pw_set("test_dev-001", "<пароль>")c.connect("exqx.containerk8s.dev.nubes.ru", 443)c.publish("test/telemetry/dev-001", '{"temp":23.5}')
+ История сообщений — REST API (Bearer-JWT):
+curl -H "Authorization: Bearer $TOKEN" \
+ "https://iot.containerk8s.dev.nubes.ru/v1/namespaces/test/iot/telemetry?limit=50"
+ mqtt), TLS{namespace}_{device_id} + пароль из APIsub, exp)
+ https://exqx.containerk8s.dev.nubes.ru — это MQTT-брокер
+ (EMQX), а не веб-страница. Точка подключения — только
+ /mqtt через WebSocket. Открытие этого адреса в браузере
+ или HTTP-запрос вернёт 400 — это штатное поведение
+ (нет WebSocket-подпротокола mqtt).
+
+ Ограничения: размер сообщения — до ~250 КБ (лимит SQS 256 КБ); + платформа может разрывать внешние WebSocket-соединения примерно раз + в 150 секунд — устройства должны автоматически переподключаться + (для QoS 1 повторы выполняются клиентом). +
+