[2026-09-23] eagle: family/projects/watchlist-automation.md personal/plans/lampac-watched-to-watchlist.md personal/tech/home-network-config.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-23 21:27:47 +06:00
1 parent ec49b14ac3
commit 8b87784327
3 files changed
+255 -44

No files matched your search

+56
View File
@@ -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` и играет
+184 -42
View File
@@ -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.
+15 -2
View File
@@ -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 (краткая выжимка)