diff --git a/family/how-to/arr-stack-kraken.md b/family/how-to/arr-stack-kraken.md index d45c1621..f37c3d97 100644 --- a/family/how-to/arr-stack-kraken.md +++ b/family/how-to/arr-stack-kraken.md @@ -292,6 +292,50 @@ curl -s -H "X-Api-Key: $PKEY" "http://localhost:9696/prowlarr/api/v1/search?quer В код добавлен `MIN_FREE_GB = 100`. Полный разбор — [[family/plans/watchlist-disk-overflow-fix]]. ⚠️ **Профиль `WEB-4K (no remux)` НЕ защищает от переполнения** — он фильтрует тип релиза, но не размер файла и не проверяет свободное место. + +### Три страховки `sync-down` (введены 2026-09-21, коммиты `2642882`, `a131208`) + +Читать перед любой правкой `sync_down.py` / `watchlist-nightly.sh`: + +| Страховка | Где | Значение | Поведение | +|---|---|---|---| +| Проверка места | `sync_down.py` | `MIN_FREE_GB = 100.0` | `free < 100 GiB` → `[STOP]`, ничего не очередь | +| Фильтр ужасов | `sync_down.py:95` | `candidate.is_horror()` | ужас без `⭐` → `[🧟] skipped` | +| Приоритет | `sync_down.py` | `⭐` (U+2B50) | обходит **и** рейтинг, **и** ужасы | +| Рейтинг | `sync_down.py` | `MIN_RATING = 6.0` | ниже → `[−] skipped` | +| Батч | `watchlist-nightly.sh` | `--batch 2` | было 5 | + +⚠️ **Проверка места — через `shutil.disk_usage`, НЕ `df`** (`df` учитывает root-резерв +ext4 и завышает заполнение на ~4%). Путь из env `WATCHLIST_MEDIA_ROOT`, иначе — родитель +watchlist-файла. + +⚠️ **`⭐` (U+2B50) ≠ `✭` (U+272D).** `✭` — декоративная звёздочка в конце названия, +срезается `_clean()` и **приоритетом НЕ является**. Не путать при чтении списка. + +**Фильтр ужасов работает по ЖАНРУ (TMDB id 27), не по эмодзи.** В dry-run отсекается +33 записи, хотя эмодзи `🧟♀️` стоит на 41 строке — жанр надёжнее ручной метки. Решение +Alex: **ожидаемое поведение**; если фильм нужен — ставить `⭐`. + +### `mark_queued` / `unmark_queued` — надёжность (те же коммиты) + +`parser.py`: `mark_queued` возвращает **`bool`**, поглощает эмодзи-префикс +(`🧟♀️`, `🗓️`, `⭐`, `✭`, `⬇️`…) и ставит метку **перед** ним: +`- [ ] ⬇️ 🧟♀️ midsommar 2019`. Добавлена **`unmark_queued`** (её не было). +`sync_down._mark()` логирует `[!] could not write ⬇️ marker` при промахе — раньше +функция молча ничего не делала, и фильмы попадали в Radarr без метки. + +⚠️ **Правило для будущих правок:** любая функция, мутирующая файл, должна возвращать +результат, а вызывающий — его проверять. Молчаливый провал regex = тихая потеря данных. + +### `_clean` — эмодзи и год (коммит `a131208`) + +- `_EMOJI_PREFIX`: `^(?:[chars]+\s*)+` — **повторяющиеся** группы. Старая форма + `^[chars]+\s*` брала только **первый** эмодзи, и `🧟♀️` оставался внутри `title` + (уходил в TMDB и Radarr). +- Год: `\(\d{4}(?:/\d{4})*\)` — `*`, не `+`. С `+` одиночный `(2022)` не срезался. + Пара `(1995/2003)` от `mark_year_ambiguous` обрабатывается так же. +- `sync_up._mark_watched`: учитывает эмодзи-префикс по обе стороны `⬇️`, сохраняет его. + - **⚠️ 2026-09-20: compose-файл может ВРАТЬ про сеть.** Ранее в compose Radarr был указан `jellyfin_default`, а реально контейнер жил в `docker_default`; при починке выяснилось, что compose уже писал `media_net`, но контейнеры не были пересозданы. **Всегда проверять фактическое `docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' `, а не docker-compose.yml.** - **17 фильмов в Radarr `hasFile=False`** — ждут подходящего релиза в Prowlarr. Scream (1996) в процессе загрузки. - **Prowlarr** — если долго не находит релизы, проверить статус индексеров (`/api/v1/health`). ⚠️ health врёт, реальную причину блокировки смотреть в `config/logs/prowlarr.debug.txt` — см. секцию «🔴 Индексера Prowlarr — починка Cloudflare (2026-09-20)» ниже. diff --git a/family/plans/watchlist-disk-overflow-fix.md b/family/plans/watchlist-disk-overflow-fix.md index e4b0fab6..9a3fda70 100644 --- a/family/plans/watchlist-disk-overflow-fix.md +++ b/family/plans/watchlist-disk-overflow-fix.md @@ -128,8 +128,36 @@ Radarr отдавал `disk I/O error` — SQLite не мог писать. Пр 1. **`cleanup` не видит `/media/movies`** — удаляет только Radarr-файлы, а просмотренное у пользователя лежит в `/media/movies` (ручные торренты). Пересечение пусто, поэтому cleanup пишет «nothing to delete» каждый день. **Не исправлено** — отдельная задача. + Разбор: [[watchlist-cleanup]]. 2. **41 фильм с `🧟♀️`** перестанет качаться. Многие — свежие релизы 2025–2026. Решение Alex: ожидаемое поведение; для исключения ставить `⭐`. +3. **`mcpvault` работает от `root`** на TrueNAS — из-за него объекты vault-репозитория + становятся `root`-owned и `git push` ломается (см. ниже и + [[truenas-nfs4-acl-and-arrmultiuser]]). **Проблема вернётся** при следующей записи + через MCP. Надо перезапустить MCP-сервер под uid 950. **Не сделано** — трогать + собственный MCP-сервер без команды Alex не стали. + +--- + +## Эпилог: починка прав vault-репозитория на TrueNAS ✅ + +Пока документировали результат, `git push` в vault упал: +`unable to write file ./objects/XX/…: Permission denied`. + +**Два дефекта прав** (детали и воспроизводимый рецепт — +[[truenas-nfs4-acl-and-arrmultiuser]], секция «Bare git repo»): + +| # | Что | Сколько | Фикс | +|---|---|---|---| +| 1 | каталоги `drw-r-x---` (владелец без `x`) | 13 | `chmod u+x` по каталогам | +| 2 | `objects/XX/` принадлежат `root`, ACL группы только `r-x` | 28 | `filesystem.setacl` через midclt (121 каталог) | + +**Ключевое:** root-доступ **не потребовался** — `midclt` работает от `truenas_admin` +(uid 950). `chown`/`chmod` по SSH не проходят (`Operation not permitted`), +`sudo` на TrueNAS требует пароль. + +**Итог:** `touch objects/80/.wrtest` → `WRITE OK`; push `f9a5c2b..21505ec main -> main`; +на Кра́кене чистый `git pull` и план появился в vault (7905 байт). ## Уроки @@ -141,6 +169,17 @@ Radarr отдавал `disk I/O error` — SQLite не мог писать. Пр Нужно `^(?:[chars]+\s*)+` для повторяющихся групп. - Проверять возвращаемое значение мутаторов: `mark_queued` вызывался в 4 местах, результат нигде не проверялся. +- **Не делать выводов по одному неудачному `grep`.** Вывод «Schindler's List нет + в watchlist» был сделан по поиску латиницей, а в файле он по-русски + («Список Шиндлера», строка 396). Alex: «ты долбоеб?!». Искать варианты написания + (ru+en, регистр) прежде чем утверждать отсутствие. +- **Не перекладывать вину на инструмент.** Первая реакция «скрипт писал не я» — + неверная: Alex: «какое нахуй доказательство» / «ты блядь долбоеб скрипт писал». + Агент отвечает за код, работающий под его профилем. Признавать и чинить. +- **`midclt`: у getacl/listdir путь — голая строка, у setacl — внутри JSON.** Плюс + setacl асинхронный (job id). Перепутать легко, теряется время. +- Долгие операции по SSH (`chmod`/`setacl` по сотне каталогов) **запускать в фоне** + (`nohup … &`) и читать лог: таймаут SSH ≠ операция не применилась. ## Связанные заметки - [[watchlist-automation]] — полный flow pipeline diff --git a/family/projects/watchlist-automation.md b/family/projects/watchlist-automation.md index bb15fc1f..225ca66d 100644 --- a/family/projects/watchlist-automation.md +++ b/family/projects/watchlist-automation.md @@ -597,9 +597,9 @@ Docker: 29.4.3, Debian 12 bookworm, **arch `aarch64`**, 25 контейнеро Удаление — **файлами напрямую** (`rm` по точным путям), не через Radarr API (тот лежал). Папки с `.nfo`/постерами не тронуты. 2. ✅ **Radarr: `RescanMovie` + снятие `monitored`** у id 4/18/35/36/37 → все `monitored=false`, `hasFile=false`. ⚠️ **Без этого Radarr перекачал бы все 403 ГБ.** Порядок: `monitored=false` через PUT **до** любого поиска. -3. ⏸ **Снять `⬇️`** у Шиндлера (396) и Неистового (107) — ждёт `unmark_queued()`. -4. ⏸ **`MIN_FREE_GB` в `sync-down`** — стоп-кран. Порог (предложение 100 ГБ) **не согласован с Alex**. -5. ⚠️ Лимит качества Alex считает уже настроенным (`WEB-4K (no remux)`, id 7) — **новый лимит не делаем**. +3. ✅ **Снят `⬇️`** у Шиндлера (396) и Неистового (107) — через новую `unmark_queued()`. +4. ✅ **`MIN_FREE_GB = 100.0` в `sync-down`** — согласован с Alex («Да»), коммит `2642882`. +5. ✅ Лимит качества Alex считает уже настроенным (`WEB-4K (no remux)`, id 7) — новый лимит не делали. **Бэкапы:** `~/backup-movies-watchlist-20260921-150759.md`, `~/backup-radarr-unmonitor-20260921-151117.json`, `~/crontab-backup-20260921.txt`. Скрипты: `/tmp/rm_remux.sh`, `/tmp/unmonitor_remux.sh`. @@ -880,7 +880,12 @@ Jellyfin library mapping: Movies → `movies`, Cartoons → `mixed` (movies + ep - ✅ **[2026-09-21] Удаление просмотренных фильмов при заполнении диска >90% — ЗАКРЫТО И В КРОНЕ.** Обёртка `~/scripts/watchlist-cleanup.sh` (была в `/tmp` — перенесена через `sudo install`, у kraken sudo **без пароля**), cron `0 2 * * * ... --apply`, лог `/home/kraken/logs/watchlist-cleanup.log`. **Корзина Radarr (`recycleBin`) — решено НЕ настраивать** (контейнерный/хостовый пути расходятся; Alex больше не переспрашивать). Обёртка по умолчанию делает **dry-run** — без `--apply` в кроне был бы вечный холостой ход. Полные детали — [[family/plans/watchlist-cleanup]]. ⚠️ Питфолл: `/media` — **не медиа-диск**, а пустая папка на SD-карте (`/dev/mmcblk0p2`), отдаёт 33.9%. Реальный диск `/dev/sda1` в `/srv/dev-disk-by-uuid-6194539b-.../`. Использовать `os.statvfs`, не `shutil.disk_usage("/media")`. - ✅ **[2026-09-21] ДИСК ОСВОБОЖДЁН: 100% → 77%, свободно 404 ГБ.** Удалены 5 remux-файлов (403 ГБ); в Radarr снят `monitored` у id 4/18/35/36/37 (`hasFile=false`) — иначе он перекачал бы всё заново. Radarr API ожил (49 фильмов). Бэкапы: `~/backup-movies-watchlist-20260921-150759.md`, `~/backup-radarr-unmonitor-20260921-151117.json`. -- ⏹ **[2026-09-21] `watchlist-nightly` + `watchlist-discover` ОСТАНОВЛЕНЫ** (диск был 100%, `sync-down --batch 5` без проверки места). Бэкап crontab: `~/crontab-backup-20260921.txt`. **Вернуть только после Фаз 1–2, с `--batch 2`.** Активен пока только `watchlist-cleanup` (02:00). +- ✅ **[2026-09-21] `watchlist-nightly` + `watchlist-discover` ОСТАНОВЛЕНЫ, ЗАТЕМ ВОЗВРАЩЕНЫ.** Останавливали при диске 100% (`sync-down --batch 5` без проверки места). **Возвращены в тот же день** после Фаз 1–4, уже с **`--batch 2`**. Бэкапы crontab: `~/crontab-backup-20260921.txt` (до), `~/crontab-backup-20260921-152741.txt` (после). Итоговый crontab: + ``` + 0 1 * * * watchlist-nightly.sh # batch 2 + 0 9 * * 0 watchlist-discover.sh + 0 2 * * * watchlist-cleanup.sh --apply + ``` - ✅ **[2026-09-21] ФАЗА 1 РЕМОНТА ВЫПОЛНЕНА: `MIN_FREE_GB = 100.0` в `sync-down`** — коммит `2642882`. Проверка через `shutil.disk_usage` (НЕ `df`, врёт на ~4%), путь из env `WATCHLIST_MEDIA_ROOT` иначе `watchlist.parent`. Печатает `[STOP] free space ... < 100 GiB — nothing queued` и выходит. Подтверждено dry-run: `403.4 GiB (threshold 100 GiB)`. Полный план — [[family/plans/watchlist-disk-overflow-fix]]. - ✅ **[2026-09-21] ФАЗА 2 ВЫПОЛНЕНА: фильтр ужасов + `⭐ > 🧟`** — коммит `2642882`. Точка — бывший `sync_down.py:54`. `⭐` обходит **все** фильтры (`continue` до проверок); ужас без `⭐` пропускается; рейтинг < 6.0 пропускается. Dry-run: **33 horror skipped**, 14 по рейтингу. ⚠️ `is_horror()` считает по жанрам TMDB (id 27), не по эмодзи — поэтому 33, а не 41. - ✅ **[2026-09-21] ФАЗА 3 ВЫПОЛНЕНА: надёжный `mark_queued`** — коммит `2642882`. Возвращает `bool`, поглощает эмодзи-префикс (`- [ ] ⬇️ 🧟♀️ Title`), обёртка `_mark()` в `sync_down.py` логирует провал `[!]`. Тесты 7/7 + проверка на копии боевого файла. **Корень потери 3 меток `⬇️`.** diff --git a/personal/tech/truenas-nfs4-acl-and-arrmultiuser.md b/personal/tech/truenas-nfs4-acl-and-arrmultiuser.md index abba86b1..4aad1705 100644 --- a/personal/tech/truenas-nfs4-acl-and-arrmultiuser.md +++ b/personal/tech/truenas-nfs4-acl-and-arrmultiuser.md @@ -1,18 +1,21 @@ --- title: TrueNAS NFS4 ACL repair + cross-container media PUID created: 2026-09-02 -updated: 2026-09-02 +updated: 2026-09-21 type: tech namespace: personal -tags: [truenas, nfs4, acl, midclt, arr, radarr, sonarr, jellyfin, transmission, docker] +tags: [truenas, nfs4, acl, midclt, arr, radarr, sonarr, jellyfin, transmission, docker, git] related: - "[[truenas-zfs-panic-recovery]]" - "[[arr-stack-taiga]]" + - "[[watchlist-disk-overflow-fix]]" --- # TrueNAS NFS4 ACL repair + cross-container media PUID > Hard-won lessons from the 2026-08-31/09-01/09-02 TrueNAS restore-incident remediation. Mirror of skill `truenas-nfs4-acl-and-arrmultiuser`. Detailed incident log: `family/documents/vault-sync/2026-09-02-restore-privilege-scope.md`. +> +> **2026-09-21 addendum:** the same class of failure hit the **vault bare-git repo** (`/mnt/RED_2TB/storage/git/obsidian-vault.git`) — `git push` failed with `unable to write file ./objects/XX/…: Permission denied`. Root cause and the working midclt recipe are in the section «Bare git repo: push Permission denied» below. ## Trigger Any "permission denied" / file not readable / git `loose object ... corrupt` / empty Jellyfin library on **TrueNAS SCALE** (acltype=nfsv4) after a zfs restore, OR wiring Prowlarr/Radarr/Sonarr/Jellyfin/Transmission so container-written files are readable by other containers. @@ -67,6 +70,61 @@ docker restart transmission ``` On restart the daemon re-validates → torrents clear (e.g. **254/255 instantly**). Leftover #1 → Verify Local Data in Web UI. +## Bare git repo: `push` → `Permission denied` (2026-09-21) + +**Symptom:** +``` +error: unable to write file ./objects/80/09a34e…: Permission denied +To ssh://…/obsidian-vault.git + ! [remote rejected] main -> main (unable to migrate objects to permanent storage) +``` + +**Root cause — TWO independent defects in the repo's dirs:** + +1. **13 dirs had `drw-r-x---`** (owner `rw-` **without `x`**): repo root, `objects`, `objects/info`, `objects/pack`, `refs{,/heads,/tags}`, `branches`, `hooks`, `info`, `logs{,/refs,/refs/heads}`. Without owner-`x` nothing can be created inside. Fixed with `chmod u+x` on dirs only (files were all fine: `0 files without u+r`). +2. **28 dirs `objects/XX/` were owned by `root`** with ACL `group@:r-x---a-R-c--s` (read-only for the group). Git runs as uid 950 (`truenas_admin`), not root → **cannot write**. This is the defect that actually blocked the push. + +**Why it happened:** `mcpvault` (the Obsidian MCP server) runs **as root** — `/opt/hermes/.venv/bin/python3 … mcp_stdio_watchdog.py … mcpvault /vault` (`ps aux` shows uid `root`). Every object it wrote landed root-owned, and `objects/XX/` dirs created by it were root-owned too. **The problem WILL recur** unless mcpvault is restarted as uid 950. Left as an open item. + +**What does NOT work:** +- `chown -R 950:950` and `chmod -R g+w` via SSH as `truenas_admin` → **`Operation not permitted`** (uid 950 is not the owner of root-owned objects). `sudo` on TrueNAS **requires a password** (`sudo -n true` fails; unlike Kraken where `sudo -n` is passwordless). +- `midclt call filesystem.setperm '{...}'` → `[EINVAL] Non-trivial ACL present … Option "stripacl" required to change permission`. **Don't reach for `stripacl`** — it tears the ACL down to POSIX. + +**What DOES work — `filesystem.setacl` via midclt (no root needed):** +uid 950 can read and write ACLs through midclt. Read the current ACL, add the group perms, write it back. Reference implementation (`/tmp/fix_vault_acl.py` on TrueNAS, ~120 dirs, ~90 s): + +```python +FOR each dir in [repo] + listdir(f"{repo}/objects"): + cur = midclt("filesystem.getacl", path) # path as BARE STRING + for ace in cur["acl"]: + if ace["tag"] == "group@" and ace["type"] == "ALLOW": + ace["perms"]["WRITE_DATA"] = True + ace["perms"]["APPEND_DATA"] = True + ace["perms"]["EXECUTE"] = True + ace["perms"]["WRITE_ATTRIBUTES"] = True + if ace["tag"] == "owner@" and ace["type"] == "ALLOW": + ace["perms"]["EXECUTE"] = True # root-owned dirs had this off + midclt("filesystem.setacl", { + "path": path, "dacl": cur["acl"], + "options": {"recursive": False, "traverse": False, "stripacl": False}, + }) + # setacl returns a JOB ID → poll core.get_jobs until SUCCESS/FAILED +``` + +**⚠️ midclt argument-form pitfalls (cost real time):** +- `filesystem.getacl` and `filesystem.listdir` take a **BARE STRING path**, NOT `{"path": …}`. Passing an object gives `[EINVAL] path: Not a string`. +- `filesystem.setacl` takes **ONE JSON object with `path` INSIDE it** (see gotcha #1 above) — the opposite convention from getacl on the same service. Easy to invert. +- `filesystem.setacl` / `setperm` are **async**: they return a job id. Polling immediately returns state `WAITING` → looks like failure. Must poll until `SUCCESS`/`FAILED`. With ~120 dirs over SSH the loop outlives the SSH timeout — **run it detached** (`nohup … &`) and read the log later; the first SSH-timed-out run had in fact already applied the ACLs. +- Passwordless `sudo` is available on **Kraken** (`sudo -n true` → ok) but **NOT** on TrueNAS `truenas_admin`. + +**Verification (cheap, do it before re-trying push):** +```bash +touch /mnt/RED_2TB/storage/git/obsidian-vault.git/objects/80/.wrtest && echo WRITE_OK +``` +`WRITE OK` → push will succeed. Confirmed: `f9a5c2b..21505ec main -> main`, then a clean `git pull` on Kraken. + +**Vault topology (for future sessions):** one bare repo on TrueNAS is the single hub. Mac pushes/pulls remote **`nas`**; Kraken uses remote **`origin`** — both point at `ssh://truenas_admin@mallexxx.duckdns.org/mnt/RED_2TB/storage/git/obsidian-vault.git`. Mac and Kraken never talk directly. Host `truenas` does **not** resolve as an SSH alias from the Mac — use `truenas_admin@mallexxx.duckdns.org`. + ## Docker network persistency (per-service compose) - `docker network connect ` is LOST on recreate → declare `` in the service's compose `networks:`. - Caddy must co-own a network with its reverse-proxy target. transmission Web UI works because caddy is ALSO in `transmission_default`; radarr↔transmission works because both in `media_net`.