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 @@ Nubes IoT Admin + + + + +
+ nubes + Nubes + · IoT + + {{VERSION}} +
+ +
+ +
+ nubes +
+

IoT тестирование

+

Приём и хранение телеметрии устройств: 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} — чужие топики + отклоняются брокером. +

+
+
+ +
+
Порядок действий — 3 шага
+
+ +
+
1
+
+
Регистрация устройства
+

Устройство создаётся через 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/devices
+ curl -H "Authorization: Bearer $TOKEN" \ + https://iot.containerk8s.dev.nubes.ru/v1/namespaces/test/iot/devices/dev1 +
+
+
+ +
+
2
+
+
Подключение устройства к MQTT
+

Endpoint wss://exqx.containerk8s.dev.nubes.ru/mqtt, + подпротокол WebSocket — mqtt; username + {namespace}_{device_id}, пароль из шага 1; публикация в + топик {namespace}/telemetry/{device_id}.

+
+ import paho.mqtt.client as mqtt
+ c = 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}') +
+
+
+ +
+
3
+
+
Чтение телеметрии
+

История сообщений — REST API (Bearer-JWT):

+
+ curl -H "Authorization: Bearer $TOKEN" \ + "https://iot.containerk8s.dev.nubes.ru/v1/namespaces/test/iot/telemetry?limit=50" +
+ Открыть консоль → +
+
+ +
+
+ +
+
Подключение
+
+
MQTT endpoint
+
wss://exqx.containerk8s.dev.nubes.ru/mqtt
+
Протокол
+
MQTT 3.1.1 / 5.0 over WebSocket (subprotocol mqtt), TLS
+
REST API
+
https://iot.containerk8s.dev.nubes.ru
+
Аутентификация MQTT
+
username {namespace}_{device_id} + пароль из API
+
Аутентификация REST
+
Bearer JWT (sub, exp)
+
Топик устройства
+
{namespace}/telemetry/{device_id}
+
Консоль
+
/console — устройства, эмулятор, телеметрия
+
Админ-статистика
+
/iot-admin (токен ADMIN_STATS_TOKEN)
+
Health
+ +
+
+ +
+
MQTT endpoint (exqx)
+
+

+ https://exqx.containerk8s.dev.nubes.ru — это MQTT-брокер + (EMQX), а не веб-страница. Точка подключения — только + /mqtt через WebSocket. Открытие этого адреса в браузере + или HTTP-запрос вернёт 400 — это штатное поведение + (нет WebSocket-подпротокола mqtt). +

+

+ Ограничения: размер сообщения — до ~250 КБ (лимит SQS 256 КБ); + платформа может разрывать внешние WebSocket-соединения примерно раз + в 150 секунд — устройства должны автоматически переподключаться + (для QoS 1 повторы выполняются клиентом). +

+
+
+ + + +
+ + diff --git a/internal/service/api/ui/static/favicon.svg b/internal/service/api/ui/static/favicon.svg new file mode 100644 index 0000000..11a9ece --- /dev/null +++ b/internal/service/api/ui/static/favicon.svg @@ -0,0 +1,55 @@ + + diff --git a/internal/service/api/ui/static/logo.svg b/internal/service/api/ui/static/logo.svg new file mode 100755 index 0000000..d76eef6 --- /dev/null +++ b/internal/service/api/ui/static/logo.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + +