Commit Graph
100 Commits
Author SHA1 Message Date
Repinoid 4c2f2e0e86 chore: update ignore rules 2026-10-27 14:08:52 +03:00
Repinoid eeee7a5ea2 docs(history): §11 — фикс валидации namespace, doc/device-integration.md, образ v0.1.14 2026-10-11 20:29:40 +03:00
Repinoid 52a93da95d docs: интеграция устройства — MQTT/ACL, требование ts/seq в payload (дедуп по содержимому), лимиты 2026-10-11 20:26:30 +03:00
Repinoid 6b9f397746 fix(api): валидировать {namespace} во всех эндпоинтах (было только в POST) + регрессионные тесты 2026-10-11 20:26:30 +03:00
Repinoid 4c24c6351c docs(history): расширенный прогон PASS=49 KNOWN=1 — дедуп по содержимому, валидация namespace только в POST 2026-10-11 20:23:32 +03:00
Repinoid fa16b02455 docs(history): проверки развёрнутого IoT — PASS=12 FAIL=0 (auth/ACL/tenant-изоляция/дедуп) 2026-10-11 19:49:33 +03:00
Repinoid 8e35f9fe5c docs(history): e2e телеметрия пройдена (API -> wss MQTT -> EMQX -> SQS -> consumer -> tenant PG -> API) 2026-10-11 19:45:37 +03:00
Repinoid 351d8e7c34 docs(history): деплой IoT на Nubes ✅ — EMQX + iot-service созданы, /health /ready = 200 2026-10-11 19:40:09 +03:00
Repinoid 58cede5716 docs(history): правка дефолтов SQS_ENDPOINT/MQTT_HOST 2026-10-11 19:33:28 +03:00
Repinoid 26621f5d08 fix(config): дефолт SQS_ENDPOINT -> https://sqs.sinefo.ru; MQTT_HOST теперь обязателен (пустой дефолт — EMQX в другом namespace) 2026-10-11 19:33:15 +03:00
Repinoid e1804c9b97 fix(deploy): MQTT_HOST = containerk8s.<uid> (сервис платформы не называется resource_name) + разбор падения iot-service (PG: владелец таблиц) 2026-10-11 19:21:12 +03:00
Repinoid 5978d80ab2 docs(deploy): требование ровно одного EXPOSE + iot-emqx v0.2.5 в чек-листе 2026-10-11 15:01:53 +03:00
Repinoid 582902e47a fix(emqx): образ с одним EXPOSE (scratch) + конфиг без salt_rounds — платформа требует ровно один порт 2026-10-11 15:01:31 +03:00
Repinoid 0832d7e0cb feat(deploy): Terraform-стек IoT (nubes_http: EMQX + iot-service) — провайдер nubes-test 3.1.0 2026-10-11 14:20:13 +03:00
Repinoid c06fb3a8e2 docs: PG для IoT готов — user iotservice + db iotdb созданы через Nubes API (test) 2026-10-11 14:03:38 +03:00
Repinoid e0a5c77f16 docs: чек-лист деплоя IoT на Nubes (образы Gitea, SQS очередь+DLQ, env) 2026-10-11 10:07:22 +03:00
Repinoid 73dc51b7f5 chore: игнорировать *.key (креды PG/секреты) 2026-10-11 09:54:19 +03:00
Repinoid 16b9f05a86 build: registry → gitea.services.ngcloud.ru/nail (Makefile/Dockerfile/дока) 2026-10-11 09:51:08 +03:00
Repinoid fdb9fa86b8 docs(history): зафиксировать решения по оставленным пунктам (16/12) 2026-10-11 09:19:50 +03:00
Repinoid f781de75cf docs(history): внешнее ревью и исправления 2026-10-11 2026-10-11 09:14:12 +03:00
Repinoid e510e93099 iotpg: валидировать namespace (safeNamespaceRe) в getTenantDB на всех путях чтения/записи 2026-10-11 08:49:00 +03:00
Repinoid 4c1b9b7b9e IoT: maxReceiveCount ко всем ошибкам consumer, ждать Shutdown до deferred Close, не 500 после успешного create 2026-10-11 08:48:29 +03:00
Repinoid 93d47376b1 bridge: MessageID=хэш(дедуп), классификация permanent/transient SQS-ошибок, бесконечный ретрай подписки, валидация ns/deviceID 2026-10-11 08:46:22 +03:00
Repinoid 89b5660a8b iotpg: не возвращать ошибку при постоянной (poison) ошибке flush — данные уже обработаны построчно 2026-10-11 08:37:17 +03:00
Repinoid 994fc054b1 console: namespace = sub (убрать sless-SHA256) и удалить несуществующий /ensure 2026-10-11 08:06:20 +03:00
Repinoid fc26ca1954 docs(HISTORY): хвосты 2026-10-11 закрыты, осталось обновлено 2026-10-11 07:55:35 +03:00
Repinoid e83d804501 test(iot): безопасность enqueue↔closeAndWait под race (фикс 🔴3)
Параллельный enqueue + закрытие канала не должен паниковать «send on closed
channel». go test -race ./... зелёный.
2026-10-11 07:55:08 +03:00
Repinoid ba14d4a767 fix(iot): консоль — убрать старые домены kube5s/sless
- apiBase → window.location.origin (same-origin с API: консоль и /v1 раздаёт один
  iot-service).
- mqttBase → wss://exqx.containerk8s.dev.nubes.ru/mqtt (как в landing.html).
- Убраны упоминания «оператор»/k8s в комментариях; MQTTX-инструкция — новый хост.
2026-10-11 07:53:02 +03:00
Repinoid 3e1d981614 fix(iot): пустое тело сообщения → DLQ (вместо молчаливого подтверждения)
Убран ранний дроп пустого тела; теперь оно идёт через processTelemetry →
errInvalidEnvelope → DLQ (или дроп после maxReceiveCount, если DLQ недоступна).
2026-10-11 07:51:57 +03:00
Repinoid c3106d6e20 fix(iot): ReceivedAt в RFC3339Nano (порядок телеметрии, non-FIFO)
bridge пишет, consumer парсит RFC3339Nano (субсекундная точность). Раньше RFC3339
(секунды) + non-FIFO очередь → показания в одну секунду могли встать не в том
порядке. Parse RFC3339Nano совместим со старыми RFC3339-сообщениями (доля секунды
опциональна).
2026-10-11 07:51:38 +03:00
Repinoid 099900580b docs(HISTORY): журнал сессии 2026-10-10 — легаси, ветка master, ревью и фиксы
Полная трассировка: чистка легаси, переименование в master, правки по ревью Opus,
PRO-ревью (3 находки), Этап 3/4/5, P0-3 bcrypt, тесты, открытые находки (§5),
осталось (§6).
2026-10-10 19:29:06 +03:00
Repinoid 85bcf26220 chore(iot): MaxReconnectInterval 30с + Debug для health-проб и телеметрии (ревью)
- bridge: SetMaxReconnectInterval(30с) — дефолт ~10м слишком долог для телеметрии.
- logging middleware: /health,/healthz,/ready → Debug (не засорять лог).
- processor: 'telemetry buffered' вместо Info 'saved' (данные ещё буферизованы).
2026-10-10 19:24:12 +03:00
Repinoid aac36bab39 docs(iot): деплой-дока — пароль отдаётся один раз, в БД bcrypt-хэш 2026-10-10 19:23:25 +03:00
Repinoid 7c80c7b7f8 test(iot): юнит-тесты валидации идентификаторов и разбора DSN (P1-11)
- handler/devices_test.go: validateIdentifier (charset/лимиты, запрет _ + # /).
- iotpg/dsn_test.go: replaceDSNDatabase (вкл. sslrootcert со слэшем, ошибка для
  key=value) + safeNamespaceRe.
- gofmt для затронутых файлов. go test ./... зелёный.
2026-10-10 19:23:09 +03:00
Repinoid 3a73207abd fix(iot): 409 различает конфликт name vs device_id (ревью)
store.Create классифицирует 23505 по имени constraint: (namespace,device_id) →
ErrDeviceIDExists, иначе ErrAlreadyExists. Handler отдаёт разные 409-сообщения.
Помогает Terraform понять, какое поле конфликтует.
2026-10-10 19:21:47 +03:00
Repinoid 3b7c8a0a63 feat(iot): bcrypt-хэш паролей устройств и бриджа + EMQX на bcrypt (P0-3 ч.1)
- handler CreateIoTDevice: пароль устройства хранится bcrypt-хэшем (hashPassword),
  открытый пароль отдаётся один раз в 201.
- store.EnsureBridgeDevice: пароль бриджа тоже bcrypt-хэшем.
- emqx/emqx.conf: password_hash_algorithm name=bcrypt, salt_rounds=10 (подтверждено
  по emqx/emqx: HOCON-ключ salt_rounds; Go bcrypt.DefaultCost=10).
- Добавлена зависимость golang.org/x/crypto v0.42.0.
2026-10-10 19:21:10 +03:00
Repinoid 59bd743c87 feat(iot): PATCH обновляет metadata (Terraform)
iotDeviceUpdateRequest получил поле metadata; UpdateIoTDevice обновляет переданные
поля (enabled и/или metadata). Добавлен store.UpdateMetadata. Хотя бы одно поле
обязательно. Раньше metadata нельзя было изменить (только enabled).
2026-10-10 19:18:01 +03:00
Repinoid 38e3f65c21 fix(iot): пагинация — tiebreaker ts DESC,id DESC + потолок limit; точный лог (Этап 5)
- QueryTelemetry: ORDER BY ts DESC, id DESC (был без tiebreaker → нестабильная
  пагинация при одинаковых ts).
- ListIoTDevices: limit дефолт 100, потолок 1000 (ошибка парсинга больше не даёт
  безлимитный запрос).
- Логи 'batch dropped' → 'валидные строки — построчно' (после fallback батч НЕ
  теряется целиком).
2026-10-10 19:17:25 +03:00
Repinoid f3927fdf11 fix(iot): готовность consumer по SQS + ограничение wg.Wait + идемпотентный Close (Этап 3)
- consumer.Run получил onReady: /ready теперь false при ошибке ReceiveMessage
  (раньше флаг consumer'а выставлялся до старта и не отражал SQS).
- main.go: wg.Wait заменён на waitWithTimeout (shutdownGrace=15с) — зависшую
  подсистему не ждём бесконечно.
- iotpg.Close идемпотентен (sync.Once) — повторный вызов не паникует.
2026-10-10 19:16:30 +03:00
Repinoid c725676f85 chore(iot): удаление сообщений пакетами DeleteMessageBatch (Этап 5)
Было по одному DeleteMessage на сообщение. Теперь DeleteMessageBatch (до 10 за
вызов) с логированием Failed-записей; хелпер deleteMessage удалён.
2026-10-10 19:15:08 +03:00
Repinoid 4a0372ff05 chore(iot): лимит тела запроса + запрет неизвестных полей + таймауты сервера (Этап 5)
- decodeJSON: http.MaxBytesReader (64 КиБ) + DisallowUnknownFields; используется
  в Create/Update устройства. Ловит опечатки в полях (важно для Terraform).
- http.Server: добавлены ReadTimeout=15с, WriteTimeout=60с, IdleTimeout=120с
  (был только ReadHeaderTimeout).
2026-10-10 19:14:47 +03:00
Repinoid e429f11e7f chore(iot): убрать мёртвый mqtt_auth.go + constant-time админ-токен (Этап 5)
- Удалён internal/service/api/handler/mqtt_auth.go: методы MQTTAuth/MQTTAcl
  нигде не подключены (мёртвый код; при подключении небезопасен — доверял
  клиентскому clientid и разрешал subscribe на '#').
- admin.go: сравнение ADMIN_STATS_TOKEN через subtle.ConstantTimeCompare.
2026-10-10 19:14:16 +03:00
Repinoid faba28f824 fix(iot): replaceDSNDatabase (смешение тенантов) + guard namespace (Этап 4)
- replaceDSNDatabase: возвращает ОШИБКУ для key=value DSN (раньше молча отдавал
  исходный DSN → телеметрия всех тенантов ушла бы в management-БД). Query
  отделяется первым '?', поэтому sslrootcert=/path больше не ломает поиск БД.
- EnsureTenantDB: guard safeNamespaceRe (defense-in-depth, совпадает с API-валидацией);
  небезопасный namespace → ErrInvalidNamespace → в consumer классифицируется как
  постоянная ошибка (DLQ, не вечный ретрай).
2026-10-10 19:13:35 +03:00
Repinoid c3492cb0f1 fix(iot): JWT — обязательный exp, 401 вместо 403, guard тест-режима, JSON-ошибки (Этап 4)
- validateJWT: exp теперь ОБЯЗАТЕЛЕН (без него токен был вечным).
- middleware: невалидный/просроченный токен → 401 (было 403) — клиент/Terraform
  отличают «протух» от «чужой namespace» (там осталось 403).
- AUTH_TEST_MODE игнорируется при заданном JWT_HMAC_SECRET (иначе обходил подпись).
- Все ошибки middleware — application/json через authErr (были text/plain).
2026-10-10 19:12:48 +03:00
Repinoid 6ff5576e7d fix(iot): /ready отражает факт подписки MQTT (находка #3)
Было: main выставлял ready.SetBridge(true) ПЕРЕД bridge.Run, поэтому «соединён,
но подписка не прошла» давал /ready=200 и молча терял телеметрию до реконнекта.
Теперь bridge сам управляет флагом (колбэк onSubscribed): true только после
успешного SUBSCRIBE, false при потере соединения/сбое подписки.
2026-10-10 19:10:44 +03:00
Repinoid d7701f2845 fix(iot): ограничить повторы ядовитого сообщения (находка #2)
Было: при недоступной DLQ битое сообщение НЕ подтверждалось и крутилось на
основной очереди бесконечно. Теперь читается ApproximateReceiveCount; после
maxReceiveCount=5 битое сообщение подтверждается (дроп с громким логом), а не
зацикливается. При доступной DLQ — уходит туда на первом приёме.
2026-10-10 19:10:12 +03:00
Repinoid dc56a9c2e2 fix(iot): run() int вместо os.Exit — финальный flush не теряется (находка #1)
os.Exit обходит defer-ы: на fatal-пути и при ошибке HTTP-сервера iotStore.Close()
(финальный flushAll последнего батча телеметрии) не выполнялся. Логика вынесена в
run() int, main = os.Exit(run()); все выходы через return — defer-ы отрабатывают.
2026-10-10 19:09:33 +03:00
Repinoid 946600238b fix(iot): ретрай GetQueueUrl на старте + выход ≠0 при сбое подсистемы (🔴5)
- Добавлен ResolveQueueURLWithRetry (10 попыток × 2с backoff): временная ошибка
  GetQueueUrl больше не роняет bridge/consumer без ретрая.
- main.go: при ПОСТОЯННОЙ ошибке bridge/consumer процесс завершается с кодом ≠ 0
  (fatal-флаг + cancel), чтобы платформа перезапустила контейнер — раньше процесс
  оставался жив, а /health отдавал 200.
2026-10-10 19:05:08 +03:00
Repinoid 572988a2a1 fix(iot): слить остаток буфера bridge при shutdown + безопасное закрытие канала (🔴3)
- Диспатчер получил собственный ctx (не сигнальный): при shutdown воркеры
  успевают отправить накопленное в SQS (раньше сразу возвращали «context
  canceled» и дропали остаток). Флаш ограничен bridgeFlushTimeout=10с.
- enqueue/closeAndWait синхронизированы мьютексом (closed-флаг): поздний колбэк
  paho больше не паникует на «send on closed channel».
2026-10-10 19:02:30 +03:00
Repinoid 5600646db3 fix(iot): ретраи отправки SQS до shutdown + таймаут на попытку (🔴4)
- sendWithRetry больше не дропает после 3 попыток (~7с) при почти пустом буфере:
  ретраит до ctx.Done() (shutdown) или успеха, backoff капится на 8с.
- Добавлен sendAttemptTimeout=10с на одну попытку — зависшее соединение SQS
  не блокирует воркер навсегда.
2026-10-10 18:59:37 +03:00
Repinoid ab146861d3 fix(iot): не терять битые сообщения при недоступной DLQ (🔴7)
- ResolveQueueURL для DLQ теперь ретраится 3 раза с backoff: временная ошибка
  больше не отключает DLQ навсегда.
- Битый envelope подтверждается только если DLQ реально приняла его; при
  недоступной DLQ/сбое sendToDLQ сообщение НЕ подтверждается (вернётся после
  visibility timeout), а не дропается.
2026-10-10 18:58:26 +03:00
Repinoid 6048777a9e fix(iot): не рвать автореконнект при сбое подписки (🔴6)
OnConnectHandler вызывал c.Disconnect(100) при таймауте/ошибке подписки. В paho
это ЯВНОЕ отключение — автореконнект после него не срабатывает, и bridge
«глохнет» (соединения нет, подписки нет, /ready=true).

Теперь: подписка ретраится 3 раза с паузой 2с; при сбое — ждём следующего
реконнекта (OnConnectHandler снова вызовется). Disconnect не вызывается.
2026-10-10 18:55:34 +03:00
Repinoid 2cca7cb856 fix(iot): семантическая валидация envelope (пустые поля → DLQ)
processTelemetry теперь отбрасывает (в DLQ) envelope с пустым namespace,
device_id или payload ДО записи в БД — иначе пустой namespace создавал бы
tenant-БД «tenant_», а пустой payload давал NULL (23502)/битый JSONB и вечный
ретрай. Дополняет построчный fallback в flushTenant (🔴2).
2026-10-10 18:53:59 +03:00
Repinoid e31f88ba6b fix(iot): построчный fallback при постоянной ошибке батча (🔴2)
Одна «ядовитая» строка (NULL/пустой payload, \u0000 в JSONB, битый JSON)
ронила весь multi-VALUES батч (до telemetryBatchSize валидных строк), который
классифицировался как permanent и выбрасывался целиком.

Добавлен flushRowsIndividually: при не-retryable ошибке батча вставляем строки
по одной (ON CONFLICT DO NOTHING) — валидные сохраняются, битая логируется.
2026-10-10 18:53:34 +03:00
Repinoid 7407de5fd3 fix(iot): сериализовать flush (гонка с фоновым flusher'ом — потеря данных)
🔴1 ревью: flushAll и синхронная запись полного батча в InsertTelemetry писали
параллельно с фоновым flusher'ом (каждые 200мс). Flush() из consumer видел
пустой буфер (фоновый flusher уже забрал строки) и возвращал nil → consumer
подтверждал SQS-сообщение, пока INSERT ещё в полёте/упал.

Добавлен flushMu: сериализует grab буфера + INSERT между фоновым flusher'ом,
Flush() и полным батчом InsertTelemetry. Теперь Flush дожидается in-flight
записи и возвращает реальную ошибку.
2026-10-10 18:52:43 +03:00
Repinoid 8335afb617 fix(iot): валидация namespace/name/device_id (запрет _, +, #, /)
- Добавлен identRe ^[a-zA-Z0-9][a-zA-Z0-9-]*$ и validateIdentifier.
- CreateIoTDevice теперь проверяет namespace (≤56), name (≤64), device_id (≤64).
- Причины: _ ломает username «namespace_device_id» (auth EMQX LIMIT 1 без
  ORDER BY), +/# — wildcard в ACL-топике, / — лишний уровень топика.
2026-10-10 18:50:56 +03:00
Repinoid 2aef884d02 fix(iot): telemetry_topic в ответе + нормализация metadata
- iotDeviceResponse: добавлено telemetry_topic = «ns/telemetry/device» (явный
  топик для Terraform; topic_prefix = «ns/» был неполным).
- metadata теперь без omitempty и всегда «{}» при пустом — убран drift
  «{}» против null (ревью 2026-10-10, «Что стабилизировать для Terraform»).
2026-10-10 18:49:40 +03:00
Repinoid e2bb642df2 fix(iot): created_at в RFC3339 + перечитывание после Create
- deviceToResponse: created_at теперь RFC3339 (был «2006-01-02 15:04:05 UTC»)
  — единый формат с last_connected, без drift для Terraform.
- CreateIoTDevice: после INSERT перечитываем устройство из БД (GetByName),
  чтобы в 201 отдавался реальный created_at (in-memory структура его не имеет).
2026-10-10 18:48:52 +03:00
Repinoid 76f0230f5d fix(iot): отдавать MQTT-пароль при создании + писать phase в БД
- handler/devices.go: CreateIoTDevice отдаёт mqtt_password один раз в 201
  (было deviceToResponse(device, "") — пароль терялся, устройство нельзя
  подключить; блокер для Terraform). Заголовок поправлен (GET — без пароля).
- store/devices.go: Create пишет phase явно через phaseOf(enabled) — иначе
  enabled=false лежал в БД как 'Active' (DEFAULT), расходясь с POST/GET.
2026-10-10 18:48:07 +03:00
Repinoid 4290ecfb0a Merge remote-tracking branch 'origin/master'
# Conflicts:
#	.gitignore
2026-10-10 18:39:10 +03:00
Repinoid 56fadf6ca3 style(iot-admin): переформатирование HTML (форматтер) 2026-10-10 18:38:12 +03:00
Repinoid a510522660 chore(iot): убрать старое (Kafka/K8s) из живого админ-UI и .gitignore
- iot-admin.html: секции Kafka/Pods заменены на Очередь shared-SQS и Рантайм
  (бэкенд admin.go давно отдаёт sqs/runtime, а фронт рисовал мёртвые секции);
  удалены CSS .pod-*/.badge-*/.dot-*/.lag-bar*, исправлен заголовок.
- .gitignore: удалены сталые пути бинарников старого IoT (/iot-operator,
  /mqtt-bridge, /kafka-consumer, /sqs-consumer).
2026-10-10 18:15:13 +03:00
Repinoid b205f886b6 chore(iot): удалить легаси-код (оператор/CRD/старый API/standalone bridge)
Удалён кластер старого IoT, не импортировавшийся активным монолитом:
- cmd/iot-operator/, controllers/, api/v1alpha1/ — k8s-оператор + CRD
- internal/api/ — старый REST API + UI-embeds (заменён internal/service/api)
- cmd/mqtt-bridge/ — standalone bridge (заменён internal/service/bridge)

go mod tidy убрал k8s-зависимости (k8s.io/*, controller-runtime).
2026-10-10 18:11:05 +03:00
Repinoid ecf6b41131 docs(review): Opus code-review of iot-service (bridge->SQS->consumer) verbatim 2026-10-10 18:02:59 +03:00
Repinoid 18e4352a47 chore(P2-14/п.4): удалён legacy cmd/sqs-consumer
Не собирался Dockerfile/Makefile (только ./cmd/iot-service/) → в образ/деплой
не попадал; логика поглощена internal/service/consumer/. Был источником пустого
message_id (обход дедупа). message_id оставлен nullable — единственный пишущий
путь (bridge) всегда ставит UUID. LEGACY.md обновлён. Плюс исправлена неточность
в HISTORY про поведение без DLQ.
2026-10-07 07:50:29 +03:00
Repinoid 59ed2d59d0 doc(HISTORY): п.2 сделан; п.4 — открытая развилка (NOT NULL vs nullable+удалить legacy) 2026-10-07 07:47:36 +03:00
Repinoid e8873fa24d fix(telemetry): ts = время приёма (ReceivedAt), а не момент INSERT
Раньше ts проставлялся now() в БД при INSERT → при батчинге и параллельных
записях порядок телеметрии (ORDER BY ts DESC) ломался. Теперь в ts идёт
ReceivedAt из envelope (серверное время приёма, ставит bridge); битый/пустой
received_at → zero → стор подставляет now(). Схема не меняется (используется
существующая колонка ts). Обновлены callers: consumer/processor.go и legacy
cmd/sqs-consumer.
2026-10-07 07:47:10 +03:00
Repinoid f2768568d3 doc(HISTORY): реализация п.1 (retryable/permanent) и п.3 (DLQ) зафиксирована 2026-10-07 07:41:22 +03:00
Repinoid 4ee0fc3b61 fix(telemetry): различать retryable/permanent ошибки flush (не залипать на ядовитом батче)
isRetryableFlushErr: '08' (connection) и 57P01/02/03 (shutdown/crash) — retryable
(возврат в буфер); '22' (data exception) и '23' (integrity violation) — permanent
(батч выбрасывается с логом, НЕ возвращается в буфер). Иначе один невалидный
payload зацикливал flush namespace навсегда (вечные 200мс-ретраи). Относится к
обоим путям: flushAll и InsertTelemetry(full).
2026-10-07 07:41:05 +03:00
Repinoid 4031882cae feat(consumer): битые envelope → DLQ вместо потери
processTelemetry теперь возвращает sentinel errInvalidEnvelope при битом JSON
(раньше — молчаливый nil → mусор подтверждался и терялся). Consumer при этой
ошибке кладёт тело в DLQ (SQS_DLQ_NAME, дефолт iot-telemetry-dlq) и подтверждает
основное сообщение; без DLQ — только логирует, но НЕ подтверждает (оседает).
Добавлены SQSDLQName в config + DefaultSQSDLQName. Реальные ошибки PG (не invalid)
по-прежнему не подтверждаются и уходят в ретрай после visibility timeout.
2026-10-07 07:40:09 +03:00
Repinoid 2b3838680f doc(HISTORY): архитекторский разбор телеметрии — 5 проблем и решение 2026-10-07 07:38:27 +03:00
Repinoid b6c083e80f fix(telemetry): не терять строки при ошибке flush (flushAll + InsertTelemetry)
flushAll и InsertTelemetry (путь заполнения батча) вынимали строки из буфера и
подменяли буфер ДО записи: при ошибке flushTenant строки исчезали. Хуже —
фоновый flusher мог забрать строки, которые consumer ждал в Flush, тогда Flush
видел пустой буфер, возвращал nil, и consumer подтверждал SQS-сообщение с
незаписанными данными (потеря). Теперь при ошибке строки возвращаются в буфер
в обоих путях. ctx проброшен в flushAll/flushTenant. Consumer хранит receipt
handle значением (string), не указателем SDK.
2026-10-07 07:32:00 +03:00
Repinoid 38602b7560 P0-8: идемпотентность телеметрии (dedup по message_id)
Envelope получил MessageID (UUID); таблица iot_telemetry — столбец message_id +
UNIQUE(device_id,message_id); батч-INSERT ON CONFLICT DO NOTHING. Дубли при
at-least-once повторе из SQS больше не вставляются. Пустой message_id → NULL
(без ложного дедупа).
2026-10-07 07:26:50 +03:00
Repinoid 3036572ccc P1-9: readiness-проба /ready отражает состояние bridge/consumer
/health остаётся liveness (процесс жив); /ready возвращает 503, если bridge или
consumer не в строю (горутина упала/не поднялась). Добавлен api.Readiness с
atomic-флагами, NewRouter принимает *Readiness, main поднимает/опускает флаги
вокруг фоновых горутин.
2026-10-07 07:25:09 +03:00
Repinoid f19ab47c29 HISTORY: журнал сессии 2026-10-07 (IoT P0-1/P0-2, P1-4, P0-3.2, P0-7) 2026-10-07 07:23:52 +03:00
Repinoid 3175605931 P0-7: подтверждение из SQS только после успешного flush телеметрии в БД
InsertTelemetry лишь буферизует и возвращает nil до фактической записи. Consumer
удалял сообщение сразу после processTelemetry == nil — краш/flush-ошибка в этом
окне = безвозвратная потеря. Теперь: копим receipt handles, вызываем store.Flush,
и только при успехе DeleteMessage; при ошибке flush — не удаляем (сообщения
вернутся после visibility timeout). Добавлен iotpg.Flush().
2026-10-07 07:23:25 +03:00
Repinoid a4e786d03b P0-3 (часть 2): не отдавать пароль устройства повторно через GET
GetIoTDevice больше не возвращает mqtt_password (deviceToResponse получает ).
Пароль остаётся доступен только один раз — при создании устройства.
Хэширование паролей в БД + перевод EMQX на алгоритм хэша (часть 1) требует
изменения emqx.conf и миграции — отдельным шагом.
2026-10-06 19:06:02 +03:00
Repinoid c97b63b190 P1-4: убрать неаутентифицированные /internal/mqtt/auth|acl из роутера
EMQX ходит в PostgreSQL напрямую (backend=postgresql), эти HTTP-эндпоинты были
неиспользуемым fallback и висели на публичном порту без аутентификации (перебор
паролей устройств / выдача ACL). Удалены из роутера.
2026-10-06 19:05:12 +03:00
Repinoid 4afee5cdf9 P0-1/P0-2: tenant-граница по sub + обязательная подпись JWT
validateJWT теперь возвращает sub; middleware сверяет sub с {namespace} из URL
(несовпадение → 403) — закрывает горизонтальный доступ к чужим namespace.
Вне AUTH_TEST_MODE подпись HS256 обязательна: без JWT_HMAC_SECRET middleware
отказывает (fail-closed, 500), а config.Load требует секрет на старте.
2026-10-06 19:04:54 +03:00
Repinoid 2e52f0fa6b chore: игнорировать сборочный бинарник ws-probe 2026-10-05 20:09:38 +03:00
Repinoid 110d49effe docs(thinking): IoT↔SQS handoff — сводка находок и порядок работ 2026-10-05 20:04:20 +03:00
Repinoid 33077ad326 docs(thinking): readiness assessment, remediation plan, Opus review prompt 2026-10-05 15:53:29 +03:00
“Naeel” f76383a2ef chore: убрать __pycache__ из трекинга, расширить .gitignore (pyc/бинарники/мусор) 2026-08-17 08:20:44 +04:00
“Naeel” da4c3d10d0 docs(thinking): Nubes DevOps report — network issues + fixes 2026-08-16 20:43:03 +04:00
“Naeel” ce9e20266c feat(landing): vertical large pipeline diagram; v0.1.13 2026-08-16 19:16:58 +04:00
“Naeel” 30e7da8cf8 feat(landing): hand-drawn pipeline SVG diagram; v0.1.12 2026-08-16 19:11:17 +04:00
“Naeel” 0f71e715be docs(landing): rewrite as clear how-to (4 steps with copy-paste commands); v0.1.9 2026-08-16 18:51:59 +04:00
“Naeel” ce01b6f9f8 feat(api): landing page with Nubes design (logo+favicon), /static; v0.1.8 2026-08-16 18:46:17 +04:00
“Naeel” 68becfd6bc fix: review findings (async SQS dispatcher, backoff, batch insert, shutdown order, pagination, HMAC) + loadtest fixes; v0.1.6 2026-08-16 18:24:09 +04:00
“Naeel” 10716aeace test(loadtests): 10 load scenarios + verifier; docs: Sonnet review findings 2026-08-16 16:59:29 +04:00
“Naeel” 649a880182 docs: no-kubectl deployment procedure (tail 4) + HISTORY 28-29 2026-08-16 16:20:07 +04:00
“Naeel” 8b3f3ab5df feat(emqx): PG-auth in EMQX v0.2.4 + EnsureBridgeDevice v0.1.2 (tail 3) 2026-08-16 16:18:49 +04:00
“Naeel” 12b43e7999 fix(bridge): resubscribe on every (re)connect; emqx v0.2.1 dashboard off; iot-service v0.1.1 2026-08-16 15:24:26 +04:00
“Naeel” 85f646cfe4 fix(emqx): v0.2.0 от официальной базы + резолвимые auth-заглушки — EMQX работает (502 устранён); HISTORY: развязка задокументирована 2026-08-16 14:22:35 +04:00
“Naeel” b19e5ec638 fix(emqx): mqtt_path→websocket.mqtt_path (v0.1.4, локально OK); HISTORY: на платформе boot висит — задокументировано 2026-08-16 13:48:26 +04:00
“Naeel” b6ffd52e00 fix(emqx): v0.1.2 — procps для launcher (502 из-за ps: command not found); HISTORY: ошибки deck/502 задокументированы 2026-08-16 13:28:41 +04:00
“Naeel” 1f30d13e95 fix(emqx): v0.1.1 — сборка от debian, единственный EXPOSE 8083 (deck-UI берёт порт из образа, поле applicationUsePort в форме отсутствует) 2026-08-16 13:22:39 +04:00
“Naeel” 7d4384b79a fix(emqx): EXPOSE только 8083 + комментарий про applicationUsePort в deck-UI 2026-08-16 13:17:43 +04:00
“Naeel” 0363b8b56a feat: EMQX-образ для платформы (naeel/iot-emqx) — конфиг вшит, auth/ACL URL через env 2026-08-16 10:13:17 +04:00
“Naeel” 7647627052 feat: монолит iot-service (Фаза 1) — config/store/api/bridge/consumer без k8s, устройства в PG, новые Dockerfile/Makefile 2026-08-16 10:07:51 +04:00