После 24 спринтов regress-suite разросся; нестабильность блокирует доверие. Этот спринт: ловит flaky тесты, добавляет observability (Grafana + Prometheus alerts + RUNBOOK), сертифицирует 10× cert-прогон. 1. tests/regression/find-flaky.sh — 10× прогон + JSON-агрегатор → docs/flaky-tests.md (per-test pass/fail sequence + reproduce). 2. OrgFactory.signupWithRetry теперь honors Retry-After header (api-client.ts:ApiError.retryAfterSec). Stage rate-limit поднят: RATE_SIGNUP_HOUR=5000, RATE_PER_IP_MIN=5000 (~/food-market-stage/deploy/.env). 3. fullyParallel=true + workers=4 = тесты идут в недетерминированном порядке; isolation работает (OrgFactory per-test). 4. workers=4 даёт **2.4× ускорение** (66.6s → 27.7s). Worker-scoped фикстура lib/worker-org.ts добавлена как opt-in. 5. deploy/grafana/dashboards/quality-watchdog.json (10 панелей: smoke success ratio 7d, incidents, multi-tenant violations, current emoji, p95 by endpoint, step failures, RPS, DB p95, docs posted, disk free) + dashboards/README.md. quality-watchdog.sh пишет Prometheus textfile экспорт в ~/.fm-watchdog/textfile/quality_watchdog.prom для node_exporter. 6. deploy/prometheus/alerts.yml — 10 правил, 4 группы (uptime, errors, database, quality-watchdog). MultiTenantViolation = P0. deploy/prometheus/prometheus.yml — reference config. 7. docs/RUNBOOK.md +178 строк: action per alert (api-down, rps-drop, http-errors-spike/growing, doc-posting-errors, db-p95-high, disk-free-low, watchdog-red, multi-tenant-violation, watchdog-incident). Junior-friendly с конкретными командами. **Cert-прогон (10× workers=4):** 420/420 passed, 0 flaky, avg 30.1s/run, total 300.6s (< 5min budget). Изменения вне репо: - ~/food-market-stage/deploy/.env — RATE_* limits bumped. - ~/quality-watchdog.sh — добавлен .prom textfile экспорт. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
29 KiB
Runbook — операционные процедуры food-market
Что делать, когда что-то идёт не так, или когда нужно сделать неавтоматическую операцию.
Контактные точки
| Что | Где |
|---|---|
| Stage URL | https://test.admin.food-market.kz |
| Prod URL | https://admin.food-market.kz (план, ещё не задеплоен) |
| Stage VM | nns@192.168.1.190 (через ssh, prod-vm в локалке) |
| Dev VM (этот хост) | nns@<this> — здесь крутится локальный API/Postgres + локальный Forgejo + локальный Docker registry |
| Forgejo (primary git) | http://127.0.0.1:3000/nns/food-market.git |
| GitHub (mirror) | https://github.com/nurdotnet/food-market (только зеркало) |
| Local Docker registry | 192.168.1.193:5001 (memory: local_docker_registry) |
| Hangfire Dashboard (stage) | https://test.admin.food-market.kz/hangfire — только SuperAdmin |
| Swagger (stage) | https://test.admin.food-market.kz/swagger |
Health-чеки
| Endpoint | Что значит | Что делать при 503 |
|---|---|---|
GET /health |
Процесс отвечает | Контейнер живёт, проблема в обвязке (nginx/cert/DNS). |
GET /health/live |
Процесс жив (без проверок) | То же. |
GET /health/ready |
БД отвечает + миграции применены | См. ниже «Health/ready упал». |
GET /metrics |
Prometheus exposition | Если 404 — приложение не стартануло. |
/health/ready упал
ssh nns@192.168.1.190 'docker logs --tail 100 food-market-stage-api'— стек ошибки на старте.- Типичные причины:
- Миграция упала: ищем
Failed executing DbCommand/relation "..." already exists. Решение: миграция конфликтует со снапшотом БД. Возможно её надо переписать сIF NOT EXISTS(см.Phase6e_RetailSaleReturns.csкак пример «defensive migration»). - OpenIddict cert pass mismatch: переменная
OpenIddict__CertPasswordв docker-compose env'е не совпадает с паролем PFX-файла →CryptographicException: PKCS12 password incorrect. - Connection refused: Postgres контейнер не успел подняться.
depends_on.condition: service_healthyдолжно это покрывать, но если healthcheck не успел —docker compose restart api.
- Миграция упала: ищем
- Если фикс требует кода —
~/deploy-stage.shпосле правки.
Деплой на stage
~/deploy-stage.sh
Скрипт делает:
docker buildapi и web с локальным registry в качестве кеша.docker pushобоих образов в192.168.1.193:5001.ssh nns@192.168.1.190→docker compose -p food-market-stage pull api web→up -d --force-recreate.- Ждёт
https://test.admin.food-market.kz/health/readyдо 30с.
Важно: проект docker compose называется food-market-stage
(флаг -p food-market-stage). См. инцидент ниже про project name.
Бэкап и восстановление
Расписание
systemd-таймер food-market-backup.timer (см. deploy/) запускается
каждый день в 03:00 локального времени prod-vm. Запускается через
OnCalendar=*-*-* 03:00:00 + Persistent=true (догоняет пропущенные
если сервер был выключен).
Скрипт food-market-backup.sh:
pg_dump -Fcиз контейнераfood-market-postgres→db-<TS>.dump.tar czfкаталога/opt/food-market-data/uploads→uploads-<TS>.tgz.- Удаляет файлы старше 30 дней (
FM_BACKUP_RETENTION_DAYS).
Папка: /opt/food-market-data/backups/.
Ручной бэкап
ssh nns@192.168.1.190 'sudo /opt/food-market/deploy/food-market-backup.sh'
Или из репо разработчика:
deploy/backup.sh --remote 192.168.1.190:5434 # PG в Docker exposed на 5434
Recovery drill (RTO ≈ 25 секунд на сегодняшних данных)
Sprint 15 — verified восстановление stage'а в свежий PG-контейнер на dev-vm:
| Шаг | Время |
|---|---|
pg_dump -Fc из stage-postgres |
~2 секунды (на 1.5k чеков / 200 продуктов) |
Создать чистый Docker-контейнер postgres:16-alpine |
~1 сек |
pg_restore --clean --if-exists --no-owner --no-privileges |
~4 секунды |
| Поднять API против восстановленной БД | ~19 сек (cold-start dotnet + migrations) |
/health/ready → {"status":"Healthy"} |
подтверждено |
| Всего RTO (single-instance) | ~25 секунд |
Команды, выполненные в drill'е:
# 1. Снять бэкап со stage'а
ssh nns@192.168.1.190 'docker exec food-market-stage-postgres-1 \
pg_dump -U food_market -d food_market -Fc -f /tmp/drill.dump'
ssh nns@192.168.1.190 'docker cp food-market-stage-postgres-1:/tmp/drill.dump /tmp/drill.dump'
scp nns@192.168.1.190:/tmp/drill.dump /tmp/drill.dump
# 2. Чистый PG
docker run -d --name drill-pg \
-e POSTGRES_DB=food_market \
-e POSTGRES_USER=food_market \
-e POSTGRES_PASSWORD=drill_pass \
-p 127.0.0.1:5499:5432 postgres:16-alpine
# 3. Восстановление
docker cp /tmp/drill.dump drill-pg:/tmp/drill.dump
docker exec drill-pg pg_restore -U food_market -d food_market \
--clean --if-exists --no-owner --no-privileges /tmp/drill.dump
# 4. Проверка: API на восстановленной БД
ASPNETCORE_ENVIRONMENT=Production \
ConnectionStrings__Default="Host=localhost;Port=5499;Database=food_market;Username=food_market;Password=drill_pass" \
Hangfire__Enabled=false \
ASPNETCORE_URLS="http://127.0.0.1:5099" \
RateLimiting__Enabled=false \
dotnet run --project src/food-market.api
curl http://127.0.0.1:5099/health/ready
# → {"status":"Healthy", checks:[{"name":"database","status":"Healthy",
# "description":"БД доступна, миграции применены."}]}
# Очистка
docker rm -f drill-pg
Для прод-данных большего объёма (50k+ чеков) RTO будет ~2-5 минут — но порядок остаётся: pg_restore линейно по данным + API startup константный.
Восстановление БД из дампа
⚠️ Перезаписывает данные. Сначала остановить API.
ssh nns@192.168.1.190
cd /opt/food-market
# 1. Остановить API/Web, оставить Postgres
docker compose -p food-market-stage stop api web
# 2. Применить дамп
DUMP=/opt/food-market-data/backups/db-YYYYMMDD-HHMMSS.dump
docker exec -i food-market-stage-postgres \
pg_restore -U food_market -d food_market \
--clean --if-exists --no-owner --no-privileges \
< "$DUMP"
# 3. Поднять API обратно — миграции применятся автоматически (idempotent)
docker compose -p food-market-stage up -d api web
# 4. Проверить
curl https://test.admin.food-market.kz/health/ready
Восстановление uploads
ssh nns@192.168.1.190
cd /opt/food-market-data
sudo tar xzf backups/uploads-YYYYMMDD-HHMMSS.tgz
# Содержимое восстанавливается в текущий каталог (uploads/...)
Полный disaster-recovery (новый сервер)
- Поднять Docker, склонировать репо в
/opt/food-market. - Скопировать бэкапы в
/opt/food-market-data/backups/. - Запустить пустой стек:
cd /opt/food-market/deploy docker compose -p food-market-stage up -d postgres docker compose -p food-market-stage exec postgres pg_isready - Применить дамп (см. выше).
- Восстановить uploads.
- Запустить остальное:
docker compose -p food-market-stage up -d. - Поднять nginx + сертификат (см.
docs/stage-access.md). - Включить таймер бэкапов:
sudo cp deploy/food-market-backup.{service,timer} /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now food-market-backup.timer
Перенос на другой сервер
- На старом — снять свежий бэкап вручную.
- На новом — поднять Docker, склонировать репо, восстановить (см. выше).
- Обновить DNS A-запись
admin.food-market.kzна новый IP. - Дождаться распространения DNS (TTL).
- Старый сервер — выключить через 24 часа (для гарантии).
Смена SDK-версии
⚠️
global.jsonфиксирует8.0.417сrollForward: latestFeature. Менять только когда вышел новый patch и Microsoft анонсировал EOL текущего. memory: НЕ переключать systemwide postgres версию.
- На dev-машине:
dotnet --list-sdks— проверить что новая версия установлена. - Обновить
global.json→ новый patch. dotnet build+dotnet test.deploy/Dockerfile.api— обновитьFROM mirror/dotnet-sdk:X.Y(если тэг изменился).~/deploy-stage.sh— задеплоить, проверить/health/ready.- Verify-suite (Playwright или вручную smoke).
- Только после этого — менять на prod-машине.
Логи
| Где | Что |
|---|---|
docker logs food-market-stage-api (по контейнеру) |
Console JSON Serilog. |
/opt/food-market-data/api-logs/ (Docker volume) |
Файлы Serilog rolling. |
journalctl -u food-market-backup.service --no-pager |
Логи бэкапа. |
Hangfire Dashboard /hangfire |
Состояние фоновых джобов, истории, ошибки. |
Формат JSON-логов — структурированный, каждая запись содержит
CorrelationId, OrgId, UserId (через LogEnrichmentMiddleware).
Поиск по CorrelationId восстанавливает полный trace запроса.
Метрики
Prometheus scrape: GET /metrics (без auth). Локально в проекте нет
prometheus-сервера — на stage его тоже пока нет; план — поднять
prometheus + grafana отдельным compose'ом и proxy через nginx.
Ключевые метрики (food-market.api/Infrastructure/Observability/AppMetrics.cs):
food_market_posted_total{document_type="..."}— счётчик post'ов.food_market_unposted_total{document_type="..."}— счётчик unpost'ов.food_market_db_query_duration_seconds_*— гистограмма EF-запросов (interceptor).- Стандартные prometheus-net:
http_requests_received_total,http_request_duration_seconds,dotnet_collection_count_total, etc.
Известные инциденты
Инцидент 1: docker-compose project name
Симптом (наблюдался при первой миграции на новый stage):
docker compose pull && up -dсоздавали контейнеры с именамиdeploy-api-1вместо ожидаемыхfood-market-stage-api.- Healthcheck'и
depends_onотрабатывали по новым именам, но nginx configurated на старые — 502 Bad Gateway.
Причина: docker compose берёт project name из имени каталога,
если не указан -p. Каталог deploy/ → project=deploy → контейнеры
с префиксом deploy-. Старые контейнеры с префиксом food-market-stage-
оставались стопнутыми, новые поднялись параллельно (Docker не считает
их дубликатами потому что имена разные).
Решение: всегда передавать -p food-market-stage. Сделано в
~/deploy-stage.sh. На prod ставить аналогичный wrapper-скрипт,
не запускать docker compose голым из /opt/food-market/deploy.
Превенция: в будущем — COMPOSE_PROJECT_NAME=food-market-stage
в /etc/environment на серверах, чтобы голый docker compose тоже
не промахивался.
Инцидент 2: GHCR network flakiness
Симптом: docker push/pull в ghcr.io периодически зависает на
2-5 минут или падает по TCP-таймауту.
Причина: исходящая сеть с dev-vm к github.com нестабильна
(memory: network_github_flaky).
Решение: используем локальный Docker registry на
192.168.1.193:5001 как primary, ghcr только как mirror (для
external CI/CD когда понадобится). Stage compose тянет с локального
(REGISTRY=192.168.1.193:5001). См. memory local_docker_registry.
Инцидент 3: OpenIddict cert rotation
Симптом: после docker compose down -v (с удалением volume
api-data) OpenIddict не может расшифровать существующие refresh-токены
→ все пользователи разлогинены.
Причина: keys из App_Data/oidc-keys/ пропали вместе с volume.
Решение / превенция:
- НИКОГДА не делать
down -vна stage/prod без явного намерения. - Хранить
App_Datavolume отдельно:volumes: api-data:сexternal: true(план). - Бэкап
App_Dataвместе с БД (TODO: добавить вfood-market-backup.sh).
Инцидент 4: rate-limiter eager-config
Симптом (в integration-тестах): тесты падают с 429 Too Many Requests
после ~5 signup'ов.
Причина: RateLimiting:Enabled=true (default) читается ЭАГЕРНО при
регистрации сервисов; ConfigureAppConfiguration в WebApplicationFactory
применяется позже и не успевает override'нуть.
Решение: в integration-тестах ставим RateLimiting__Enabled=false
через переменную окружения до создания factory. Сделано в
ApiFactory static-конструкторе. Memory: test_suites_setup.
Инцидент 5: Telegram chat-id привязка
Симптом: владелец org вводит chat_id, сервер тестирует отправку →
403 Forbidden от Telegram API.
Причина: пользователь не отправил /start боту перед привязкой,
бот не может писать первым.
Решение / превенция: UI показывает инструкцию «1. Откройте бота → /start.
2. Получите chat_id у @userinfobot. 3. Введите.» Это идёт сверху на
странице привязки. Бэкенд возвращает ошибку с понятным текстом.
Инцидент 6: Identity password policy
Симптом: signup-форма принимает пароль 12345, потом
/connect/token отшивает «Invalid credentials» — потому что Identity
сам не разрешил создать пользователя с таким паролем, но контроллер
проглотил ошибку.
Превенция: контроллер AuthController.Signup теперь возвращает
IdentityResult.Errors массивом → фронт показывает причину.
Troubleshooting на стороне БД
Большой org_audit_log
prune-audit-log каждый день чистит >180 дней; если каталог-tenant
делал массовый импорт (10к товаров за раз), таблица может вырасти на
порядок. Проверка:
SELECT pg_size_pretty(pg_total_relation_size('org_audit_log'));
SELECT count(*) FROM org_audit_log WHERE created_at < now() - interval '30 days';
Ручная чистка:
DELETE FROM org_audit_log WHERE created_at < now() - interval '90 days';
VACUUM ANALYZE org_audit_log;
Stock-агрегат расходится с движениями
Инвариант: stocks.quantity = SUM(stock_movements.quantity) per
(product_id, store_id). Если разошёлся (баг где-то не вызвали
IStockService.ApplyMovementAsync):
-- найти расхождения
SELECT s.product_id, s.store_id, s.quantity AS cached,
COALESCE(SUM(m.quantity), 0) AS actual
FROM stocks s
LEFT JOIN stock_movements m
ON m.product_id = s.product_id AND m.store_id = s.store_id
GROUP BY s.product_id, s.store_id, s.quantity
HAVING s.quantity <> COALESCE(SUM(m.quantity), 0);
-- пересчитать всё (под maintenance window!)
UPDATE stocks s SET quantity = COALESCE((
SELECT SUM(quantity) FROM stock_movements m
WHERE m.product_id = s.product_id AND m.store_id = s.store_id
), 0);
__EFMigrationsHistory рассинхрон
Бывает после ручной правки миграции после её применения.
SELECT * FROM "__EFMigrationsHistory" ORDER BY 1 DESC LIMIT 5;
Если в коде есть миграция, которой нет в таблице — db.Database.Migrate()
попытается её применить (что обычно и нужно). Если в таблице есть запись,
а файла нет — обратное направление (миграция была удалена) — Migrate()
не упадёт, но фокус с EF Tools перестанет работать, см. memory
feedback_ef_migrations.
Sprint 26 — Alert response (deploy/prometheus/alerts.yml)
Каждый alert в alerts.yml имеет runbook label — anchor сюда.
Junior-разработчик находит alert в Telegram, кликает runbook_url, попадает
на соответствующий раздел.
api-down
Alert: ApiDown — up{job="food-market-api"} == 0 1 минуту.
Что значит: Prometheus не может scrap'нуть /metrics. API либо упал,
либо порт недоступен.
Действия:
curl -fsS https://test.admin.food-market.kz/health/ready— подтверди что API недоступен (или вернулся).ssh nns@192.168.1.190 'docker ps | grep food-market-stage-api'— контейнер живой?- Если контейнер в
Restarting:docker logs --tail 200 food-market-stage-api-1— стек ошибки старта (часто миграция / OpenIddict cert mismatch). - Если контейнер OK, но не отвечает:
docker exec food-market-stage-api-1 curl -s http://localhost:8080/health(внутренний порт). Если внутри отвечает, проблема в nginx/proxy цепочке. - Recovery:
cd ~/food-market-stage/deploy && docker compose -p food-market-stage up -d --force-recreate api. - Если не помогло:
~/deploy-stage.shс локального dev-vm (полный build+push+restart).
rps-drop
Alert: RpsDropped50Percent — RPS за 5 мин <50% от того же окна час
назад, при условии что было >0.5 rps.
Действия:
curl https://test.admin.food-market.kz/health/ready— API живой?ssh nns@192.168.1.190 'docker logs --tail 50 food-market-stage-api-1'— внезапные ошибки на старте/в логе.- Проверь DNS из дома/мобильной сети:
dig admin.food-market.kz— возможно сломалась запись. - Откати последний deploy:
cd ~/food-market-stage/deploy && git log -1 --format=%H && docker compose pull && docker compose up -d. Если откат на предыдущий image помог — баг в новом коде, см. логи.
http-errors-spike
Alert: HttpErrorsSpike — доля 5xx >10% уже 5 минут.
Действия:
- Logs:
ssh nns@192.168.1.190 'docker logs --tail 200 food-market-stage-api-1 | grep -iE "(error|exception)"'. - Какая ручка валится:
curl https://test.admin.food-market.kz/metrics | grep 'http_requests_received_total.*code="5'— топ-3 контроллеров. - Hangfire-failed:
curl -u admin /hangfire/jobs/failed(нужен SuperAdmin login). - Часто — БД упала. См.
db-p95-highраздел ниже. - Если баг локален (только одна ручка валится): найди фикс, deploy.
http-errors-growing
Alert: HttpErrorRateGrowing — производная 5xx растёт >10%/min 10 мин.
Действия: Постепенная деградация, не emergency. Часто — память течёт или коннекшен-пул исчерпывается.
- Memory:
docker stats food-market-stage-api-1(ratio %). - PG connections:
psql ... -c "SELECT count(*), state FROM pg_stat_activity GROUP BY state"— еслиidle in transactionмного, есть leak. - Restart api:
docker compose -p food-market-stage restart api— куплено время. - Найди корень в логах — кто часто получает Exception.
doc-posting-errors
Alert: DocumentPostingErrors — типа документов > 0.05 ошибки/сек 5 мин.
Действия:
docker logs food-market-stage-api-1 | grep "Posting failed"— ищи название документа в логе.- Hangfire failed: документы постятся через Hangfire-job — посмотри
/hangfire/jobs/failed. - Stock-инвариант:
Posting failed: stock would be negativeозначает попытку списать больше чем есть. Это бизнес-уровневая ошибка, не баг. Сообщи владельцу org. Если ошибок много — возможно баг в pre-validate. - Concurrent posting:
Posting failed: serialization conflict— это Sprint 23SerializationConflictMiddlewareловит и возвращает 409. Если не возвращает 409 а 500 — middleware сломался, проверь deploy.
db-p95-high
Alert: DbQueryP95High — p95 DB-запросов >500ms 10 минут подряд.
Действия:
- Самые медленные запросы:
SELECT calls, mean_exec_time, total_exec_time, query FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10; - Если статистика устарела:
ANALYZEвсей БД (vacuum-top-tablesHangfire-job делает это раз в неделю, см.DatabaseMaintenanceJobs). - Lock'и:
SELECT * FROM pg_locks WHERE NOT granted;— заблокирована ли какая-то таблица. - Disk: см.
disk-free-lowниже — если IO упирается в диск. - Если новый медленный запрос в логе после deploy — откати relevant контроллер.
disk-free-low
Alert: DiskFreeLow — < 5 ГБ свободно на mount.
Действия:
df -h— какой mount упал.- Logs:
du -sh /var/lib/docker/containers/*/. Логи Docker'a иногда разрастаются. Truncate:truncate -s 0 /var/lib/docker/containers/*/.log. - БД growth:
psql -c "SELECT pg_size_pretty(pg_database_size('food_market'))". Если >50 ГБ — запусти PruneStockMovements + VACUUM FULL под maintenance (см. ниже). - Quality-watchdog test orgs:
PruneQualityTestOrgsHangfire-job (cron 02:30 UTC) удаляет старыеquality-{epoch}-*org'и (см.[[sprint25_done]]). Если не отработал: trigger вручную через/hangfire. - Очисти
~/.fm-watchdog/quality.log.*старее 14 дней (auto-ротация уже есть).
watchdog-red
Alert: WatchdogLastRunRed — quality-watchdog последний прогон красный
5 мин.
Действия:
- Открой
docs/quality-status.mdв репо — сразу видно какой шаг упал. - Тот же шаг сам и воспроизведёшь:
/home/nns/quality-watchdog.sh # сразу прогоняет всё tail -50 ~/.fm-watchdog/quality.log # детали последнего шага - Дальше зависит от шага:
health— см.api-down.auth_me/products/signalr— см.http-errors-spike.multi_tenant— см.multi-tenant-violation(КРИТИЧНО).perf— см.db-p95-high.ui_flow— Playwright-тест. Прогон вручную:cd tests/regression && pnpm exec playwright test flows/03-catalog.spec.ts --grep "3.1" --headed.
multi-tenant-violation 🚨
Alert: MultiTenantViolation — шаг multi_tenant упал в последнем часу.
ЭТО P0. Org B видит данные A — это утечка между арендаторами.
Действия (немедленные):
- Останови stage'е приём новых signup'ов (косвенно — поставь
RateLimiting__SignupPerIpPerHour=0, redeploy api). tail -100 ~/.fm-watchdog/quality.log— детали leak'а (get_code,list_total).- В коде проверь
AppDbContext.ApplyTenantQueryFilter(см.[[sprint22_done]]):
Кто-то добавилgrep -n "ApplyTenantQueryFilter\|IgnoreQueryFilters" src/food-market.infrastructure/Persistence/IgnoreQueryFilters()где не надо? Это самая частая причина leak'ов. - Воспроизведи руками: создай 2 org'и (
curl POST /api/auth/signup× 2), токен для каждого, попробуй cross-access. Если воспроизводится — фикс ASAP. - После фикса —
~/deploy-stage.sh, дождись зелёного watchdog'a. - Если на prod уже катилось: notify владельцев (через Telegram-summary
job), audit-log за последние 48ч (
/api/admin/audit?since=...) на подозрительные cross-org операции.
watchdog-incident
Alert: WatchdogIncidentCreated — 2+ подряд красных прогона ⇒ incident-файл.
Действия:
ls -lt ~/.fm-watchdog/incident-*.txt | head -3— последние инциденты.cat ~/.fm-watchdog/incident-{...}.txt— описание + действия.- Server-Claude автоматически получит этот файл в очередь (через
~/fm-watchdog.sh→ ротацию queue). Не дублируй — он начнёт фикс сам. - Если хочешь форсировать вмешательство: тот же файл sent'нул в
~/.fm-watchdog/queue/0000-incident-XXX.txt(uname-prefix0000→ первый в очереди).
Что НЕ делать
- НЕ менять
global.jsonбез явного решения (CLAUDE.md). - НЕ переключать systemwide postgres версию через brew (поломает
смежные проекты в
~/Documents/devprojects/). - НЕ запускать
docker compose down -vна stage/prod (потеря volume). - НЕ делать миграции через
dotnet ef migrations add— снапшот в репо не синхронный с моделью, генератор выдаст ерунду. Пишем руками. - НЕ редактировать тот же файл одновременно с Mac-Claude (memory:
feedback_serialize_edits).