diff --git a/family/projects/watchlist-automation.md b/family/projects/watchlist-automation.md index 2f13a4aa..bfb5f0fb 100644 --- a/family/projects/watchlist-automation.md +++ b/family/projects/watchlist-automation.md @@ -12,6 +12,62 @@ tags: Полная цепочка: как `family/documents/movies-watchlist.md` превращается в скачанный контент. +## 🔴 2026-09-23: канал «Lampa → `[x]`» найден; фиксы сирот закоммичены + +### Коммит `ffe7974` — закрыт должок по грязному дереву + +В рабочем дереве `watchlist-sync` висели **незакоммиченные** правки, которые уже +работали боевым прогоном (ночной 02:30 использовал именно их): + +| Файл | Δ | Что | +|---|---|---| +| `watchlist_sync/parser.py` | +65 | фикс «ОСИРОТЕВШИЕ СТРОКИ» в `process_thumbs()`: `watched_titles`/`watched_kp` по всем секциям кроме Новинок, `_norm_title()`, вырезание сирот из `keep`, флаг `_patch_dropped` в раннем выходе | +| `watchlist_sync/cli.py` | +7 | явные ветки вывода `👍 / 👎 / [x] / [drop]` (было обобщённое `else`) | + +Закоммичены **отдельным** коммитом, не смешаны с новым кодом. +Предыдущий HEAD был `a131208` (2026-09-21). + +`sync_up._mark_watched` **уже содержит** фикс эмодзи-префикса — `_PREFIX` поглощает +эмодзи с обеих сторон `⬇️`. Работает, правок не требует, переиспользуется. + +### Найден реальный канал просмотров — модуль Lampac `TimeCode` + +`sync_up` читает **Jellyfin**, но Jellyfin больше не источник: `BaseItems` — 53, +`movie` — **0**, `UserData.IsPlayed=true` — **0**. Смотреть стали через Lampa+TorrServer. + +**Фактический источник просмотров — Lampa**, и она уже пишет их на сервер в +SQLite-БД модуля `TimeCode`: + +| | | +|---|---| +| БД | `$H/lampac/database/TimeCode.sql` (таблица `timecodes`) | +| Модуль | `/lampac/module/Sync/TimeCode/` (`Controller.cs`, `plugin.js`, `README.md`) | +| Ключ | `identity` = TMDB-ID (`movie-1758655` / `1758655_movie`) | +| Прогресс | `percent` (флага «просмотрено» нет — политика клиента, см. README) | + +🔴 **`watched_at` — НЕ признак досмотра.** Живая запись: `watched_at = 1790053739498` +при **`percent = 18`**. Значит любое обновление таймкода пишет `watched_at`. +Порог ставить **только по `percent`**. + +План реализации синка `timecodes` → `[x]`: +[[personal/plans/lampac-watched-to-watchlist]]. + +### 🔴 `ssh kraken` резолвится в мёртвый `.15` — работать через `.14` + +Все обрывы этой сессии — маршрут ssh, не сервисы. Детали и обход: +[[personal/tech/home-network-config]] → «Питфоллы доступа». + +### Состояние стека после ребута (замер 2026-09-23) + +14/14 контейнеров поднялись сами. Radarr/Sonarr queue `0`, Transmission 9 торрентов +всё `Done/Idle` (Sum down `0.0`). Диск **31%** (539G/1.8T). Lampac `:9118` → 200, +Jellyfin `:8096` → 302. Ночные скрипты отработали: cleanup 02:00, nightly 01:00, +catalog 02:30–02:32 (каналов 4, позиций 392; `.md` 708 строк / 615 позиций). + +🟡 **Лог `nightly` пишется в `/tmp/watchlist-nightly.log`** → умирает при ребуте, +след прогона не сохраняется. Остальные логи уже в `~/logs/`. Фикс предложен, +ждёт команды Alex. + ## 🔴 2026-09-22: АВТОСКАЧКА НА HDD ВЫКЛЮЧЕНА (по требованию Alex) **Контекст.** Задача — Lampa на ТВ показывает каталог из `movies-watchlist.md` и играет diff --git a/personal/plans/lampac-watched-to-watchlist.md b/personal/plans/lampac-watched-to-watchlist.md index dab3f6dc..4abdb499 100644 --- a/personal/plans/lampac-watched-to-watchlist.md +++ b/personal/plans/lampac-watched-to-watchlist.md @@ -1,7 +1,7 @@ --- title: Отметка «просмотрено» из Lampa → watchlist.md type: plan -status: awaiting-approval +status: planned-not-implemented namespace: personal created: 2026-09-23 updated: 2026-09-23 @@ -44,17 +44,35 @@ related: | Читает | `GET /timecode/all?card_id=...` | | Ключ личности | `identity` = `{tmdb_id}_movie` — **ровно то, что нужно для матча со списком** | -**Живая запись на 2026-09-23:** +🔴 **ФОРМАТ `identity` — ОБЕ ФОРМЫ СУЩЕСТВУЮТ.** В README описана `movie-674` +(дефис, префикс), но `card` выводится как `674_movie`. Живая запись показала +именно дефисную форму. Regex должен принимать обе: + +```python +m = re.match(r'^(?:(movie|tv)-)?(\d+)', str(identity)) ``` -identity = 1758655_movie card = movie-1758655 -position = 1222.0 duration = 6898.0 percent = 18.0 -watched_at = 0 deleted = 0 + +⚠️ **Живая запись 2026-09-23 (снята дважды, значения менялись):** ``` -`percent` уже считается Lampa/TorrServer. Отметка «просмотрено» в UI Lampa -кладёт `percent ≈ 100` (либо `watched_at > 0` — обе колонки проверять). +снимок 1: identity=1758655_movie card=movie-1758655 + position=1222.0 duration=6898.0 percent=18.0 + watched_at=0 deleted=0 + +снимок 2: identity=movie-1758655 card=1758655_movie + position=1222.0 duration=6898.0 percent=18.0 + watched_at=1790053739498 deleted=0 +``` +🔴 **Вывод из снимка 2: `watched_at > 0` при `percent = 18`.** Значит `watched_at` +ставится при **любой** записи таймкода, а не только при досмотре. Критерий +«`watched_at > 0` = просмотрено» — **НЕНАДЁЖЕН**. Порог должен быть по `percent`. + +⚠️ **Фильм `1758655` НЕ найден в `watchlist.json`** (368 позиций с TMDB-ID). +Это не watchlist-позиция — смотрится в Lampa из другого источника. Планируемое +поведение: не найдено в json → тихо пропустить, не ошибка. ⚠️ **`sqlite3` в контейнере НЕТ** (обычный случай у Alex) — БД читать через -`docker cp lampac:/lampac/database/TimeCode.sql /tmp/` + `python3 -c "import sqlite3 ..."`. +`docker cp lampac:/lampac/database/TimeCode.sql /tmp/` + `python3` **файлом-скриптом** +(не `-c`, каждый inline = апрув). Готовые пробы: `/tmp/probe_cache.py`, `/tmp/probe_match.py`. На хосте Кра́кена `python3` есть. ### 1.4 Порог «просмотрено» — ответ из документации модуля @@ -72,32 +90,97 @@ README модуля (`/lampac/module/Sync/TimeCode/README.md`), раздел « | `updated_at` | сервер, мс | Когда приняли строку. Курсор дельт `?since=` | | `percent` | клиент | Прогресс; флага «просмотрено» отдельно нет | -**Отсюда критерий отбора строки:** +**Отсюда критерий отбора строки — 🔴 ПЕРЕСМОТРЕН по факту (см. выше):** ``` -deleted = 0 И percent > 0 И watched_at > 0 +deleted = 0 И percent >= ПОРОГ ``` ⚠️ `deleted=1` — это **снятая** отметка (надгробие), `position`/`percent` обнулены, `duration` сохранён. Такие строки **не** считать просмотром. -⚠️ Состояние `percent=18, watched_at=0` (живая запись сейчас) = **начал смотреть, -не досмотрел** → `[x]` НЕ ставить. +🔴 **`watched_at` в критерий НЕ ВХОДИТ** — доказано живой записью: `watched_at > 0` +при `percent = 18`. Это просто «когда последний раз писали строку», ставится +любым апдейтом таймкода. + +**Порог по `percent`** — единственное надёжное. `percent = 18` = начал смотреть, +не досмотрел → `[x]` НЕ ставить. Точное значение порога (`90`? `95`?) снять +с живого досмотра: досмотреть фильм в Lampa, вычитать `percent`, записать факт сюда. ### 1.5 Что уже есть в `watchlist.json` — `tmdb_id` готов к матчу -`$H/lampac/wwwroot/watchlist.json` → `channels.*.items[]`, у **346/346** films есть `tmdb_id`. -Матч делается по TMDB-ID, **без** хрупкого сравнения названий. +`$H/lampac/wwwroot/watchlist.json` → `channels.{films,new,cartoons,anime}.items[]`. + +**Поля позиции (ВСЕ, проверено):** +```json +{"name":"Простите за беспокойство","original":"Sorry to Bother You","year":2018, + "rating":0,"image":"https://image.tmdb.org/t/p/w500/2QoxL3MMH8fvlHG7etFKfqLLnAe.jpg", + "href":"https://www.themoviedb.org/movie/424781"} +``` + +🔴 **TMDB-ID живёт ТОЛЬКО в `href`** (`.../movie/424781`). Отдельного поля `tmdb_id` +в json позиции НЕТ — это описание парсера каталога, а не json-схемы. + +🔴 **Поля с оригинальной строкой из `.md` в json НЕТ.** Alex предположил, что оно +есть — проверено, отсутствует. Значит матч json↔`.md` идёт по `name` + `year`, +и **обязан** использовать ту же нормализацию, что `parser._norm_title()` +(срезает markdown-ссылку, `(2025)`, `IMDb 7.1 KP 6.6`, приводит к `[0-9a-zа-яё]`). +Сырое имя не сравнивать — в `.md` строка выглядит так: +``` +- [ ] ⬇️ [Под огнём](https://www.kinopoisk.ru/film/5455250/) (2025) IMDb 7.1 KP 6.6 +``` + +### 1.6 Мост TMDB ↔ KP через `.resolve_cache.json` — НЕ РАБОТАЕТ + +Кэш резолвера: **483 записи, все не-null**. Структура: +```json +{"status":"ok","candidate":{"tmdb_id":412656,"kp_id":null,"title":"Поступь хаоса", + "year":2021,"media_type":"movie","score":1.0,"rating":6.548,"genres":[14,878,12]}} +``` +🔴 **`kp_id` = `null` у всех записей.** Ключ существует, значение пустое. Моста +TMDB↔KP в кэше **нет**. + +**Но он и не нужен.** `.md` хранит KP-ссылки (`kinopoisk.ru/film/`, 68 штук), +`timecodes` — TMDB-ID. Прямого моста действительно нет, **однако цепочка замкнута +через TMDB-ID**, который есть с обеих сторон: +``` +timecodes.identity (TMDB) → watchlist.json.href (TMDB) → name+year → строка .md +``` +KP в этой цепочке не участвует вообще. + +⚠️ **Питфолл разведки:** поиск «есть ли kp_id» по подстроке в JSON-блобе дал +ложное «458 записей с kp-полем» — потому что строка `"kp_id"` присутствует +с `null`. Проверять **значение**, а не наличие ключа. --- ## 2. Решение **Новый модуль в `watchlist-sync`: `sync_from_lampac.py`.** -Читает `timecodes` → сопоставляет по `tmdb_id` → ставит `[x]` в `.md` (тем же -`sync_up._mark_watched`, что уже работает для Jellyfin). Вызывается в конце -`watchlist-nightly.sh` — после `sync-up`, перед `sync-vault (push)`. +Читает `timecodes` → сопоставляет с `watchlist.json` по TMDB-ID из `href` → +берёт `name` + `year` → находит строку `.md` по нормализованному названию → +ставит `[x]` тем же `sync_up._mark_watched`, что уже работает для Jellyfin. +Вызывается в конце `watchlist-nightly.sh` — после `sync-up`, перед `sync-vault (push)`. + +**Цепочка матча (единственная рабочая):** +``` +timecodes.identity "movie-1758655" / "1758655_movie" + │ regex ^(?:(movie|tv)-)?(\d+) + ▼ + tmdb_id = 1758655 + │ watchlist.json: href = ".../movie/1758655" + ▼ + {name, year} → нормализация + год + ▼ + строка movies-watchlist.md → [x] +``` +KP-ID (`kinopoisk.ru/film/`, 68 штук в `.md`) в цепочке **не участвует** — +моста TMDB↔KP нет и не требуется. Почему так, а не «писать в Jellyfin»: Jellyfin не источник просмотров и не имеет файлов для большинства позиций. Единственный честный источник — Lampa. +**Статус на конец сессии:** план готов, код **не написан**. Разведка закрыта +(цепочка проверена, одна живая запись прогнана — матч дал 0, потому что фильм +вне вотчлиста; это ожидаемо, не баг). + --- ## 3. Шаги @@ -123,13 +206,16 @@ deleted = 0 И percent > 0 И watched_at > 0 5. **Крон.** В `~/scripts/watchlist-nightly.sh` — новый вызов **после** `sync-up`, **до** `sync-vault (push)`. Правка через `sudo cp` (`~/scripts/` — `root:root 755`). Бэкап скрипта до правки. Сумма новой версии зафиксировать в доке. -6. **Порог.** Критерий — `watched_at > 0` (документация: «просмотрено» — это - `watched_at`, а не `percent`). Живая запись сейчас `percent=18, watched_at=0` — - проходить не должна. Если в логе увидим `watched_at > 0` при `percent` ~20-40 — - значит Lampa помечает как просмотренное раньше конца; тогда добавить нижний порог - `percent >= 50` и перепроверить. +6. **Порог.** 🔴 **По `percent`, НЕ по `watched_at`** — пересмотрено по факту + (см. 1.4): `watched_at > 0` встречается при `percent = 18`. Стартовое значение + `percent >= 90`. Уточнить с живого досмотра: досмотреть в Lampa, вычитать + `percent`, вписать факт сюда. `percent = 18` проходить не должен. 7. **Документировать** в `family/projects/watchlist-automation.md` (раздел про синк) + ссылка из `personal/plans/lampac-watchlist-plugin.md`. +8. **Проверка матча — обязательна до записи.** Прогнать `/tmp/probe_match.py` + (цепочка `timecodes` → `watchlist.json` → `.md`) и **убедиться, что хоть одна + позиция находится**. Если находится 0 — матч сломан, `--apply` не запускать. + Сейчас: `1758655` в json не найден (вне watchlist) → 0 совпадений ожидаемо. --- @@ -137,14 +223,19 @@ deleted = 0 И percent > 0 И watched_at > 0 | # | Граблю | Обход | |---|---|---| -| 1 | `sqlite3` в контейнерах НЕТ | `docker cp` + `python3 -c "import sqlite3"` на хосте | +| 1 | `sqlite3` в контейнерах НЕТ | `docker cp` + **файл-скрипт** `python3 /tmp/x.py` на хосте | | 2 | `~/scripts/` на Кра́кене `root:root 755` — `scp` даёт `Permission denied` | только `sudo cp` | | 3 | Кириллица в inline-ssh | **BLOCKED** tirith — писать скрипт файлом (`write_file`), не heredoc | -| 4 | `identity` для сериалов = `{tmdb}_tv` + серия неизвестна | этап 1 — только фильмы; сериалы отдельно | +| 4 | `identity` для сериалов = `tv-1851-s1e2`, серия в `card` | этап 1 — только фильмы; сериалы отдельно | | 5 | `percent` сбрасывается в `0` при `deleted` | фильтровать `deleted = 0` | -| 6 | Матч по названию ломается на год/рейтингах | матч **только** по `tmdb_id`, строгий | -| 7 | `timecodes` пишу сам `TimeCode`, при рестарте лампака не теряется (SQLite на хосте) | ок | +| 6 | Матч по названию ломается на год/рейтингах | нормализация `_norm_title()` + год как второй ключ | +| 7 | `timecodes` переживает рестарт лампака (SQLite на хосте, не в контейнере) | ок | | 8 | Крон-файлы возвращать только `crontab /tmp/файл`; бэкап `crontab -l > f` обязателен | — | +| 9 | 🔴 **`identity` бывает в ДВУХ формах** (`movie-674` и `674_movie`) | regex `^(?:(movie\|tv)-)?(\d+)` | +| 10 | 🔴 **`watched_at` не признак досмотра** — бывает `>0` при `percent=18` | порог только по `percent` | +| 11 | 🔴 **inline `python3 -c` уходит на апрув и истекает** | все пробы — файлами | +| 12 | **`ssh kraken` резолвится в мёртвый `.15`** | работать через `kraken@192.168.1.14` | +| 13 | Длинные `ssh` + тяжёлый `jq` рвут соединение («banner exchange») | разбивать на короткие команды, тяжёлое — файлом | --- @@ -168,27 +259,78 @@ deleted = 0 И percent > 0 И watched_at > 0 --- -## 7. Открытый вопрос (задан Alex 2026-09-23, ответа нет) +## 7. Открытый вопрос — ЗАКРЫТ документацией ✅ -Как именно в Lampa ставится «просмотрено» — **вручную кнопкой** или **автоматически -при досмотре до конца**? От этого зависит порог в шаге 6: +Alex сказал «в душе не ебу как она там ставится, ищи документацию» — и был прав. +Ответ найден в README модуля, вопрос задавать не следовало. -| Как ставится | Какая колонка сработает | -|---|---| -| Вручную кнопкой | `watched_at > 0` (Lampa кладёт `updated` в `watched_at`) | -| Автодосмотр до конца | `percent >= 90` | +**README `TimeCode`, раздел «Нативные клиенты»:** -В плане заложены **оба условия одновременно** (`percent >= 90` **ИЛИ** `watched_at > 0`) — -работает в любом случае, но при ответе Alex можно сузить и убрать ложные срабатывания. +> **«Отдельного флага "просмотрено" нет: это `percent`. road Lampa его не несёт +> и не несла никогда, порог завершённости — политика клиента, а не свойство записи.»** + +То есть порог — **политика клиента** (Lampa на ТВ), сервер его не знает и не хранит. +Отсюда: единственный переносимый признак — `percent`, и значение порога надо +взять у клиента, а не у сервера. + +**Дополнительно опровергнуто фактом:** живая запись дала `watched_at > 0` при +`percent = 18`. Значит `watched_at` — «когда последний раз писали строку», +а не «досмотрел». Использовать его как критерий — ошибка. + +**Как снять точный порог (следующая сессия):** +1. Досмотреть в Lampa фильм **из вотчлиста** (не `1758655` — его нет в json). +2. Вычитать `percent` из `timecodes` (через `/tmp/probe_match.py`). +3. Вписать наблюдённое значение в этот раздел и в шаг 6. --- ## 8. Связанные находки этой сессии (не часть плана, но проверено) -- **Стек после ребута:** 14/14 контейнеров поднялись сами, Radarr/Sonarr queue `0`, - Transmission 9 торрентов всё `Done/Idle`, диск 31%, Lampac 200 / Jellyfin 302. - Ночные скрипты (cleanup 02:00, nightly 01:00, catalog 02:30) отработали. - Записано в [[family/projects/watchlist-automation]]. -- **🔴 Лог `nightly` живёт в `/tmp/watchlist-nightly.log`** → умирает при ребуте. - Сегодняшний прогон следа не оставил. Предложен фикс (перенести в `~/logs/`), - ждёт команды. Остальные логи уже в `~/logs/`. +### 8.1 Стек после ребута + +14/14 контейнеров поднялись сами, Radarr/Sonarr queue `0`, Transmission 9 торрентов +всё `Done/Idle` (Sum down 0.0), диск 31% (539G/1.8T), Lampac `:9118` → 200, +Jellyfin `:8096` → 302. Ночные скрипты отработали: cleanup 02:00, nightly 01:00, +catalog 02:30–02:32 (каналов 4, позиций 392, `.md` 708 строк / 615 позиций). +Vault git чистый. Записано в [[family/projects/watchlist-automation]]. + +### 8.2 🔴 Лог `nightly` живёт в `/tmp/` → умирает при ребуте + +Крон: `0 1 * * * bash ~/scripts/watchlist-nightly.sh >> /tmp/watchlist-nightly.log`. +`/tmp` чистится при перезагрузке — сегодняшний прогон следа не оставил, вывод +восстановлен только косвенно (по mtime артефактов). Остальные логи уже +в `~/logs/` (`watchlist-catalog.log`, `watchlist-cleanup.log`). Фикс (перенести +в `~/logs/`) предложен, ждёт команды Alex. + +### 8.3 🔴 `ssh kraken` резолвится в МЁРТВЫЙ адрес + +`ssh kraken` → DNS отдаёт **`192.168.1.15`** → порт 22 открыт, но sshd рвёт +соединение на `banner exchange`. Рабочий адрес — **`192.168.1.14`** (проверено: +`ALIVE_14`, uptime 6:27, load 0.02). + +Причина в `~/.ssh/config`: proxy-команда +``` +if ifconfig | grep -q 10.99.0.2; then nc 10.99.1.2 22; else nc kraken 22; fi +``` +На Mac интерфейса с `10.99.0.2` **нет** → уходит на `nc kraken 22` → мёртвый `.15`. +WG-туннель до Кра́кена не поднят. + +**Обход до восстановления WG:** всегда `ssh kraken@192.168.1.14` явно. +**Проверка:** `nc -z -w 5 192.168.1.14 22`. + +### 8.4 Commit незакоммиченных фиксов сирот + +В рабочем дереве `watchlist-sync` висели незакоммиченные правки, которые **уже +работали боевым прогоном**: `cli.py` (+7) и `parser.py` (+65) — фикс «ОСИРОТЕВШИЕ +СТРОКИ» в `process_thumbs()` + явные ветки вывода `👍/👎/[x]/[drop]`. +Закоммичены отдельным коммитом **`ffe7974`** (не смешаны с новым кодом). + +`_mark_watched` в `sync_up.py` **уже содержит** фикс эмодзи-префикса (`_PREFIX` +поглощает эмодзи с обеих сторон `⬇️`) — переиспользуется без правок. + +### 8.5 Внешний TMDB API с Кра́кена недоступен + +`curl https://api.themoviedb.org/3/movie/...` без ключа → `Invalid API key`. +Ключ живёт в `$WS/.env` (`TMDB_KEY`), прямой вызов из шелла не проходит. +Для проверки названий использовать `watched_at`-путь через `watchlist.json`, +а не TMDB API. diff --git a/personal/tech/home-network-config.md b/personal/tech/home-network-config.md index a7701c4d..6c3db2b3 100644 --- a/personal/tech/home-network-config.md +++ b/personal/tech/home-network-config.md @@ -50,13 +50,26 @@ route -n get 192.168.2.197 # на Mac: какой gateway? | Узел | Доступ | Статус 2026-09-23 | |---|---|---| -| Кра́кен `192.168.1.15` | `ssh kraken` (через `~/.ssh/config`) | ✅ работает | +| Кра́кен `192.168.1.15` | ~~`ssh kraken`~~ | 🔴 **СЛОМАНА**: DNS отдаёт `.15` (wlan0, metric 600) → порт 22 открыт, но sshd рвёт на `banner exchange`. Рабочий адрес — `192.168.1.14` | +| Кра́кен `192.168.1.14` | `ssh kraken@192.168.1.14` | ✅ **работает** именно так (eth0, активный) | | ASUS `192.168.1.1` | ~~`ssh -i ~/.ssh/id_rsa admin@192.168.1.1`~~ | ❌ **НЕ работает**: `Permission denied` → `Connection closed`. `nc -z 192.168.1.1 22` → closed. Маршруты — только веб-UI | | HTPC `192.168.1.86` | `ssh -i ~/.ssh/id_rsa bazzite@192.168.1.86` | (не проверялся в этой сессии) | | TrueNAS | `ssh truenas_admin@mallexxx.duckdns.org` | ✅ работает | | OpenWrt `192.168.2.2` | `ssh root@192.168.2.2` (прямой, по ключу) | (не проверялся в этой сессии) | -⚠️ **У Кра́кена ДВА IP** — `.14` (eth0, активный default metric 100) и `.15` (wlan0, metric 600). `~/.ssh/config` резолвит `kraken` на `.15`. При построении маршрутов учитывать оба. +⚠️ **У Кра́кена ДВА IP** — `.14` (eth0, активный default metric 100) и `.15` (wlan0, metric 600). **Работать через `.14`.** + +🔴 **`ssh kraken` (голый хостнейм) НЕ РАБОТАЕТ с 2026-09-23.** Причина — proxy-команда в `~/.ssh/config`: +``` +if ifconfig | grep -q 10.99.0.2; then nc 10.99.1.2 22; else nc kraken 22; fi +``` +На Mac интерфейса с `10.99.0.2` **нет** (WG-туннель до Кра́кена не поднят) → ветка `else` → `nc kraken 22` → DNS отдаёт мёртвый `.15` → `kex_exchange_identification: Connection closed by remote host`. + +**Обход:** всегда `ssh kraken@192.168.1.14` (или `ssh -o ... kraken@192.168.1.14`). +**Проверка живости:** `nc -z -w 5 192.168.1.14 22 && echo OK`. +**Заодно:** `ssh kraken` идёт через proxy, поэтому тяжёлые команды и длинные `jq` рвут соединение заметно чаще — дробить команды. + +🔴 **Симптом-ловушка:** обрыв SSH выглядит как «хост упал» / «контейнеры лежат». **Сначала проверить адрес и `ping`/`nc`**, а не лезть в `docker ps`. В этой сессии так и было: хост пинговался и был жив, load 0.02, `uptime 6:27` — сломан был только маршрут ssh. ## Прочие заметки из skill (краткая выжимка)