[2026-09-26] eagle: family/how-to/jellyfin-config.md personal/plans/balda-mm-endtoend-plan.md personal/projects/balda/balda-kraken-mm-deploy.md personal/tech/balda-go-fx-pitfalls.md personal/tech/music-tags-repair-truenas.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-26 13:31:13 +06:00
1 parent bf7fa3569d
commit 6ae7f397ef
5 files changed
+688 -86

No files matched your search

+155 -3
View File
@@ -1,12 +1,15 @@
# Balda — Go/fx pitfalls и метод правок (зеркало скилла)
> Создано 2026-09-26. Обновлено 2026-09-26 (ночь, root-cause сессия) — добавлены:
> **WS-авторизация MM** (§«авторизация — ОДИН механизм»), **`Token:`-заголовок мёртв в MM 11.x**,
> **формат JSON дельт** (плоский массив + 3 аргумента у обёртки).
> Создано 2026-09-26. Обновлено 2026-09-26 (сессия реализации MM-хендлера) — добавлены:
> **`bad_seq` — настоящая причина тишины WS** (§«WS-фрейм без seq»),
> **`optional:"true"` скрывает отсутствующую зависимость** (§«Дефект 5»),
> **`canonicalSubject` и неизвестный транспорт** (§«auth: subject-based»).
> Уточнён §«авторизация — ОДИН механизм»: челлендж НЕ был причиной №1, им был `bad_seq`.
> Зеркало секции скилла `devops/docker-balda-service`, добавленной 2026-09-26.
> Источник: работа над Mattermost-адаптером Балды (Фаза 2) + деплой на Kraken (Фаза 3).
> Проектные доки: [[personal/projects/balda/balda-mattermost-connector]],
> [[personal/projects/balda/balda-kraken-mm-deploy]],
> [[personal/plans/balda-mm-endtoend-plan]],
> [[personal/projects/balda/balda-mm-delegation-plan]].
---
@@ -231,6 +234,155 @@ go test -race ./...
---
## 🔴🔴 Дефект 5: `optional:"true"` прячет отсутствующую зависимость (ингресс-приёмник)
Найден 2026-09-26 (Фаза 3, сессия реализации). **Самый коварный из дефектов транспорта:**
приложение стартует нормально, событий не обрабатывает, и **ни один гейт этого не видит**.
**Симптом:** событие `posted` доходит до ингресса, логируется как
`mattermost post accepted for processing` — и дальше тишина. Ни ошибки, ни warn'а.
**Причина:** в `mattermostfx/ingress.go`:
```go
type IngressParams struct {
fx.In
Processor mattermost.InboundProcessor `optional:"true"` // ← никто не предоставляет
…
}
```
В `handlersfx/` были только `zulip_ingress.go` и `telegram_ingress.go` — **MM-хендлера не
существовало**. fx не падает на отсутствующей `optional`-зависимости, поэтому ингресс
получал `Processor == nil` и молча выходил:
```go
if s.processor == nil {
return // ingress.go, processPosted
}
```
📌 **`optional:"true"` — это утверждение «отсутствие допустимо», и его надо осознанно
подтверждать.** Если порт обязателен для работы транспорта — маркер должен быть **снят**,
тогда fx упадёт на старте и дефект будет виден сразу.
**Правило для нового транспорта:** у каждого порта в `*fx`-модуле проверить, **кто его
предоставляет**. Если никто — транспорт не работает, даже если `docker ps` показывает `Up`.
**Фикс:** `handlersfx/module.go` —
```go
fx.Annotate(
newMattermostInboundHandler,
fx.As(new(baldamattermost.InboundProcessor)),
),
```
(дельта `tools/deltas-mattermost-19-wire.json`).
---
## 🔴 auth: subject-based авторизация — новый транспорт надо регистрировать в трёх местах
Авторизация Balda проверяет **subject** вида `<channel>:<principal>` (`telegram:101`,
`zulip:42`, `slackagent:<team>:<user>`). Для нового транспорта это три отдельные правки,
и **забытая любая из них даёт молчаливый отказ авторизации** (не ошибку!):
| Файл | Что добавить | Симптом при пропуске |
|---|---|---|
| `auth/channel_token.go` | `Channel<X> = "<name>"` + в `CreateMissingOwnerBindTokens` | транспорт не получает owner-bind токен |
| `auth/canonical_users.go` | ветку `case Channel<X>:` в `canonicalSubject` | 🔴 `unsupported transport "<x>"` → **все** `IsOwnerSubject`/`GetCollaborator` = false |
| `auth/owner.go` | функцию `<X>Subject(id)` | нет канонического билдера subject'а |
⚠️ `switch channelType` в `canonicalSubject` заканчивается `default: return error`.
Неизвестный транспорт — **не panic**, а тихая ошибка, которую вызывающий код часто
проглатывает (`if err != nil { return false }`). Искать надо по `canonicalSubject`.
📌 Приятное: `userSubject()` в `actors/command/start/handler.go` для нового транспорта
**уже работает** через default-ветку `return transport + ":" + trimmed` — команду `/start`
править не нужно.
✅ **Проверка, что авторизация доехала:** в логах хендлера `settlement_stage` перестаёт быть
`"unauthorized"`. До привязки владельца он именно такой — **это норма, а не баг**.
---
## 🔴 WS-фрейм без `seq`: MM перестаёт пушить события, НЕ разрывая сокет
**Это была настоящая причина «connected, но тишина»** (найдена 2026-09-26 диагностическим
логом каждого кадра, дельта `deltas-mattermost-15-diag.json`).
```json
{"status":"FAIL","error":{"id":"api.web_socket_router.bad_seq.app_error",
"message":"Invalid sequence for WebSocket message."}}
```
раз в 30 с — ровно на каждый пинг. Балда слала `{"action":"ping"}` **без `seq`**.
**Механизм:** Mattermost требует **монотонный `seq` в каждом клиентском фрейме**. На
невалидном он помечает соединение сломанным и **прекращает доставлять `posted`**, при этом
**сокет не закрывает** — поэтому `mattermost websocket connected` в логе остаётся, а событий нет.
`seq` начинает требоваться с **первого** кадра. Если auth-челлендж убран (см. ниже), то
счётчик надо начинать самому — иначе **ни один** исходящий фрейм не несёт `seq`.
**Фикс** (дельта `deltas-mattermost-16-seq.json`):
```go
type Ingress struct {
…
seq int64 // монотонный счётчик исходящих фреймов
}
func (s *Ingress) nextSeq() int64 {
s.mu.Lock(); defer s.mu.Unlock()
s.seq++
return s.seq
}
// Единственный путь отправки клиентского фрейма.
func (s *Ingress) writeEvent(conn *websocket.Conn, action string, data any) error {
frame := map[string]any{"seq": s.nextSeq(), "action": action}
if data != nil { frame["data"] = data }
return conn.WriteJSON(frame)
}
```
+ сброс `s.seq = 0` при установке нового соединения;
+ пинг через `s.writeEvent(conn, websocketActionPing, nil)`;
+ фрейм со `"status":"FAIL"` — **warn**, не игнор (иначе ошибка невидима).
✅ Проверено: `bad_seq` — 0 вхождений, в логах `pong seq_reply:1, 2, 3`.
📌 **Правило:** любая отправка в MM-сокет — **только** через `writeEvent`. «Сырой»
`conn.WriteJSON`/`WriteMessage` без `seq` = тихо убитая подписка.
---
## 🔴 Диагностика: «пробник видит событие» ≠ «приложение видит событие»
Отдельный урок метода, стоивший двух сессий. Пробник подключается **своим сокетом** и события
получает; сокет приложения к тому моменту может быть уже «отравлен» (в нашем случае — `bad_seq`)
и MM на него не пушит. Из этого родилась **ложная версия** «событие приходит, но теряется
внутри `readLoop` → `handleFrame`».
**Правильный диагностический приём:** временный лог **каждого кадра** в самом ингрессе:
```go
s.logger.Debug().
Int("message_type", frame.messageType).
Int("payload_bytes", len(frame.payload)).
Str("payload_head", headOf(frame.payload, 160)).
Msg("mattermost websocket frame received")
```
+ лог в каждой точке отказа (`dispatchPosted`, фильтры, `MentionsBot`) — тогда «тишина» в
логах перестаёт быть загадкой. Именно этот лог и вскрыл `bad_seq` за один прогон.
📌 Логи уровня `debug` **не удалять** после фикса: они уже окупились и окупятся снова.
Точки отказа в `mattermost`-ингрессе: dial (с HTTP-статусом!), `frame received`,
`event decoded`, `status:FAIL`, каждый `dropping posted event: <причина>`.
---
## 🔴 `go-arch-lint` — обязательный шаг для нового транспорта
⚠️ **Это не «опциональная придирка»: `AGENTS.md` прямо требует обновлять