diff --git a/docs/hotfix-invoice500-403-progress.md b/docs/hotfix-invoice500-403-progress.md index 01ed99a..9aa5f73 100644 --- a/docs/hotfix-invoice500-403-progress.md +++ b/docs/hotfix-invoice500-403-progress.md @@ -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 `, `-o StrictHostKeyChecking=accept-new`, `-o UserKnownHostsFile=`, `-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 ` — ограниченный доступ только с 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 не трогал. +