docs(hotfix): финал 7/7 + root cause ssh/claude + file retention policy
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run

Второй проход после ручного ретеста владельца: добавил RemoteHost ssh на
dev-vm, объяснил почему e2e п.4 не поймал (не запустил), и описал
owner-policy про удаление файлов + cleanup legacy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-05 01:05:07 +05:00
parent 0ff93b6daa
commit 7745d018af

View file

@ -147,4 +147,75 @@ E2E_ADMIN_URL=https://test.admin.food-market.kz pnpm exec playwright test \
## 5. Stage retest + итог
_(заполняется)_
### Второй проход: coord-уточнение после ручного ретеста владельца
Владелец после первого фикса (п.1) загружал настоящее фото и получил:
```
Ошибка распознавания:
An error occurred trying to start process 'claude' with working directory '/app'.
No such file or directory
```
**Root cause.** В контейнере `food-market-stage-api-1` CLI `claude` НЕ установлен; extractor имел ветку `RemoteHost` по ssh (из `~/ref/sklad`), но env-переменная не была задана → ветка не бралась, экстрактор пытался запустить локальный `claude`.
**Почему мой e2e п.4 это не поймал.** Я добавил сам spec, но запустил только `menu-walk` в разделе 5-первом проходе. `stage-ui-invoice-upload.spec.ts` не был прогнан до этого фикса. Сам spec ДЕЙСТВИТЕЛЬНО ловит эту ошибку — текст `"Распознавание упало в failed: An error occurred trying to start process 'claude'..."` появляется в assert'е. Урок: регресс-проверка существует, если её запустили.
**Фикс (коммит `0ff93b6`).**
1. `ClaudeCliInvoiceExtractor.cs` — передаёт в `ssh`/`scp` опции `-i <SshKey>`, `-o StrictHostKeyChecking=accept-new`, `-o UserKnownHostsFile=<SshKnownHosts>`, `-o BatchMode=yes`, `-o ConnectTimeout=10`. Два новых конфига: `Invoices:ClaudeCli:SshKey`, `Invoices:ClaudeCli:SshKnownHosts` (default `/tmp/.fm-ssh/known_hosts` — writable путь, т.к. `/root/.ssh` в контейнере read-only).
2. `deploy/Dockerfile.api` — `+openssh-client` в runtime stage.
3. На prod-vm (192.168.1.190): сгенерирован ed25519-ключ `~/food-market-stage/deploy/secrets/invoice-ocr-key[.pub]` (не в репо; `secrets/` под `./deploy/`).
4. На dev-vm (192.168.1.192, где установлен и залогинен `claude`): `~/.ssh/authorized_keys` + строка `from="192.168.1.190",no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-pty <pub>` — ограниченный доступ только с prod-vm.
5. `docker-compose.yml` на prod-vm (не в репо):
```yaml
environment:
Invoices__ClaudeCli__RemoteHost: ${INVOICE_REMOTE_HOST:-nns@192.168.1.192}
Invoices__ClaudeCli__Executable: /home/nns/.local/bin/claude
Invoices__ClaudeCli__SshKey: /app/.ssh/invoice-ocr-key
Invoices__ClaudeCli__SshKnownHosts: /tmp/.fm-ssh/known_hosts
volumes:
- ./secrets/invoice-ocr-key:/app/.ssh/invoice-ocr-key:ro
```
`Executable` — полный путь, т.к. non-interactive non-login ssh не наследует `~/.profile`.
**Прогон на stage (после фикса):**
```
Running 4 tests using 1 worker
[invoice-upload] id=f4d9e256-dacf-4a47-b454-b79ad9538ab5 initial status=pending
[invoice-upload] final status=needs_review err=-
✓ 1 ... invoice upload full-cycle — demo-keeper … (31.6s)
✓ 2 ... menu-walk … demo-admin — reload каждого пункта меню, 0 ошибок (1.9m)
✓ 3 ... menu-walk … demo-keeper — reload каждого пункта меню, 0 ошибок (1.1m)
✓ 4 ... menu-walk … demo-cashier — reload каждого пункта меню, 0 ошибок (29.2s)
4 passed (4.1m)
```
### Файловая политика (owner decision 2026-10-05)
Фото/сканы накладных на сервере **хранить запрещено**. После распознавания (успех + ошибка) файл удаляется.
**Что сделано:**
- `InvoiceProcessor.ProcessAsync` — `DeleteStored(up)` сразу после `ExtractAsync` (успех) и в catch (ошибка). Хелпер затирает `up.StoredPath = ""`, безопасно к гонкам и падениям FS.
- `ClaudeCliInvoiceExtractor.ExtractAsync` — `rm -f` временной копии на RemoteHost в `finally` (как и было, подтверждено).
- `InvoiceWorker` — удалён периодический `SweepAsync` + поле `_lastSweep`. Конфиг `Invoices:KeepFilesDays` больше не читается; оставлен как no-op с комментарием в XML-доке.
- На stage prod-vm `/opt/food-market-data/uploads/invoices` — все накопленные legacy-файлы удалены вручную (4 шт от 18:57–19:36, до фикса); `invoice_uploads.StoredPath` обнулён для всех записей с terminal-статусом.
**Верификация:** после двукратного e2e-прогона в `/opt/food-market-data/uploads/invoices` — 0 файлов, в БД — 0 записей с непустым `StoredPath`.
### Итог 7/7
| # | Пункт | Статус |
|---|---|---|
| 1 | 500 на upload (UpdatedAt NOT NULL) | ✅ fix + integration-test на реальной Postgres |
| 2 | Фоновые 403 (presets, employees, telegram) | ✅ 3 места у источника + silent-GET-403 политика |
| 3 | menu-reload regression e2e | ✅ в quality-watchdog (step 9) + 3/3 green на stage |
| 4 | Real invoice upload e2e | ✅ pending → needs_review на stage |
| 5 | deploy-stage + retest + итог | ✅ 4/4 e2e green |
| 6 | Claude CLI via ssh на dev-vm | ✅ RemoteHost config + ключ смонтирован ro |
| 7 | Не хранить фото накладных | ✅ delete-on-done + sweep удалён + legacy cleanup |
Прод не трогал, global.json не трогал, nginx gateway не трогал.