ShowcaseSeeder — чистые данные для публичных скриншотов: 200 товаров без
Article-маркеров, реальные EAN-13, боевой формат номеров документов
(П-2026-…, ПР-2026-…), 3 поставщика, счета (касса/Kaspi/эквайринг),
сотрудники с нормальными именами, ~30 дней продаж, приёмки, 2 списания,
1 завершённая инвентаризация. Без e2e/test/Y1/DEMO в видимых данных.
- POST /api/admin/seed-demo?showcase=1 (DemoSeedController)
- повторяемый deploy/showcase-seed.sh (signup + seed, идемпотентно)
- YearDemoSeeder: data-массивы → internal для переиспользования
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Замечание владельца: инвентаризация стояла «Проведено» без оприходования/списания.
Переделано по МойСкладу (docs/inventory-moysklad-reference.md):
- InventoryStatus Draft→Completed (было Posted; значение 1 то же). Инвентаризация
остатки НЕ двигает: endpoint complete замораживает строки без движений.
- create-enter (излишки diff>0) / create-loss (недостача diff<0) — создают черновики
Оприходования/Списания со строками разницы + ссылка InventoryDocId. Повторно нельзя (409).
- Кнопки «Применить результаты» НЕТ (поправка Mac-Claude — не одобрено владельцем).
- unpost («вернуть в черновик») отменяет проведение и удаляет связанные документы.
- post/unpost — временная совместимость для мобильного «FM Склад» (post = завершить +
создать и провести оба документа); помечено в docs/mobile-api.md.
- Enter/Loss: поле InventoryDocId (+ миграция Phase38) + в DTO «на основании инвентаризации №…».
- InventoryResult (application) — чистая разбивка diff→излишки/недостача (unit-тесты:
разница/сумма/только посчитанные строки/округление).
- InventoryMigrationController (dry-run/apply): старые Posted+InventoryAdjustment →
связанные проведённые документы переметкой движений (не задваивает), с проверкой
неизменности остатков. Скрипт deploy/migrate-inventory-to-docs.sh (backup + запуск).
- Демо-сидеры: инвентаризации теперь Completed без прямых движений.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
БЕЗОПАСНОСТЬ:
- Страница входа больше НЕ префилит e-mail/пароль dev-админа и не показывает
подсказку «Dev admin: …» — только в import.meta.env.DEV на localhost
(на стенде/проде поля пустые).
- DevDataSeeder: пароль SuperAdmin/демо-админа ТОЛЬКО из секрета
(Seed:SuperAdminPassword / env SEED_SUPERADMIN_PASSWORD). Дефолт «Admin12345!»
допустим лишь в Development. На stage/prod (ASPNETCORE_ENVIRONMENT=Production)
без секрета аккаунт с известным паролем НЕ создаётся (warning в лог).
- stage-compose: проброшен Seed__SuperAdminPassword (без дефолта).
- (Пароль существующего SuperAdmin на стенде сменён вручную на сложный и сохранён
в ~/.fm-watchdog/stage-superadmin-password; quick-login по sentinel/master цел.)
ТОСТ 404:
- StageBanner и QuickLoginBlock пробуют необязательный /api/dev/quick-login/accounts
через raw fetch (минуя axios-интерсептор) — на проде/без API 404 больше не
показывается глобальным тостом. На странице входа тостов ошибок нет.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Верхняя полоса и экран входа: логотип-изображение ALL MARKET из ресурсов
(avares Assets/), color на светлой теме и white на тёмной (Brand.ApplyLogo).
- Заголовок окна «ALL MARKET · Касса»; иконка окна и ApplicationIcon = allmarket.ico
(собрана из фирменных PNG 32/180/192).
- Чек: в шапке — название точки/магазина (резолвится из RetailPoint, PosSession.StoreName),
внизу мелко «ALL MARKET — система учёта торговли» (был хардкод «Food Market» в шапке).
- AppImage .desktop: Name «ALL MARKET Касса» + фирменная иконка.
- Эталон-макет pos-mockup.html — логотип «all market».
- AssemblyName/exe (foodmarket.Pos) и имена артефактов не менялись (инфраструктура).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Тег pre-rebrand-all-market на дорибрендинговом main (c8c0be5), запушен.
- docs/rebrand-all-market-progress.md — план и границы (что НЕ трогаем).
- docs/rebrand-rollback.md — точные команды отката кода (revert-диапазон) и стенда.
- deploy/rebrand-rollback.sh — dry-run по умолчанию, --apply для реального отката.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stage-reset-demo.sql — очистка орги от транзакционных данных (KEEP каталог/
сотрудники/точки). fix-internal-ean13.sql — пересчёт неверной контрольной цифры у
уже выданных внутренних EAN-13 (на стенде исправлен 41 код, список — docs/ean13-fixed-20261008.txt).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
05.10.2026 общий postgres-инстанс с чужими боевыми базами (sklad, purchase,
calcman, calcman_staging, makesales + мусорная старая food_market) вынесен
из compose прода food-market в отдельный compose-проект shared-postgres
(контейнер shared-postgres, порт 5434, том food-market-postgres-data external).
Прод food-market поднимается в собственный контейнер food-market-prod-postgres
(порт 5437, том food-market-prod-postgres-data). Простой при разделении — 12с,
соседи живы. См. docs/shared-postgres-followup-progress.md.
Этот коммит синхронизирует репозиторий, чтобы следующий деплой из main не
воскресил старую раскладку:
- deploy/docker-compose.yml: container_name/port/volume приведены к прод-файлу
(food-market-prod-postgres, 127.0.0.1:5437, food-market-prod-postgres-data).
- deploy/shared-postgres/ (новое): документационная копия соседнего compose'а,
с явной пометкой «food-market им не управляет».
- deploy/food-market-backup.sh / anonymize-prod.sh / db-schema-diff.sh:
defaults FM_PG_CONTAINER / FM_PROD_CONT = food-market-prod-postgres.
- docs/RUNBOOK.md: новый раздел «Postgres на prod-vm» с таблицей трёх
инстансов и явным запретом трогать shared-postgres из наших скриптов.
- docs/backup-restore.md: контейнер в таблице и примерах — новый.
Guards (не в репе — живут в ~/deploy-stage.sh и ~/quality-watchdog.sh)
расширены симметрично: стенду запрещены shared-postgres, food-market-prod-
postgres[-data], 5434/5437; проду — food-market-postgres-data (чужие данные).
Проверка: docker compose config/ps на prod-vm shared-postgres не упоминают,
grep по репе — ни один скрипт не делает docker exec в shared-postgres.
Третья утечка stage↔prod: stage-compose разделял с прод-compose
/opt/food-market-data/uploads И названные тома (food-market-postgres-data,
food-market-api-data, food-market-api-logs). Теперь всё разведено.
- Новый deploy/docker-compose.stage.yml — шаблон стендовского стека с
food-market-stage-* волюмами, bind /opt/food-market-stage-data/uploads,
container_name food-market-stage-*, OpenIddict Issuer по дефолту смотрит
на test.admin.food-market.kz (а не на admin.food-market.kz).
- На prod-vm сделаны: pg_dump стенда → down → переименованы stage-*-pre-
isolation-20261005 → up с новыми волюмами → pg_restore. Прод-контейнеры,
прод-том food-market-postgres-data и прод-папка /opt/food-market-data/
не тронуты (read-only inspect).
- ~/deploy-stage.sh получил guard до билда: проверяет шаблон репо и
удалённый stage-compose на запрещённые паттерны прод-путей/томов/имён/
issuer-URL.
- ~/quality-watchdog.sh +step 10 (06:XX ежесуточно) — тот же набор
паттернов + docker inspect реального стенда.
- docs/stage-prod-isolation-progress.md: таблица до/после, что оставили,
как разводили, follow-ups.
Smoke на test.admin.food-market.kz: /health/ready Healthy, 31233 товара,
invoice upload → parsing (OCR Hangfire job), GDPR org-export zip упал в
/opt/food-market-stage-data/uploads/exports/. Прод-папка /opt/food-market-
data/uploads/ до/после миграции: 13 файлов, 0 изменений.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Owner caught (повторно, после фикса 500): контейнер stage-api пытался
запустить "claude" в своём cwd (/app), а binary там нет —
"No such file or directory". Включаем RemoteHost SSH:
1. ClaudeCliInvoiceExtractor: передаёт в ssh/scp опции -i <SshKey>,
-o StrictHostKeyChecking=accept-new, -o UserKnownHostsFile=<SshKnownHosts>,
-o BatchMode=yes, -o ConnectTimeout=10. Конфиги Invoices:ClaudeCli:SshKey
и Invoices:ClaudeCli:SshKnownHosts.
2. deploy/Dockerfile.api: + openssh-client в runtime.
3. (compose + ssh-ключ на prod-vm не в репо: deploy/secrets/invoice-ocr-key,
authorized_keys на dev-vm с from="192.168.1.190",no-pty,no-forwarding;
env RemoteHost=nns@192.168.1.192, Executable=/home/nns/.local/bin/claude.)
Owner-policy: фото/сканы накладных НЕ хранить на серверах. InvoiceProcessor
удаляет StoredPath сразу после распознавания — успех и неудача; remote
tmp копия на dev-vm подчищается в finally. InvoiceWorker больше не содержит
sweeper (периодическая очистка не нужна), конфиг Invoices:KeepFilesDays
игнорируется (оставлен в комментарии для backwards compat).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Координатор отротировал мастер-пароль стенда: теперь значение живёт в
~/.fm-watchdog/stage-shared-password на dev-vm (не коммитится) и в
QUICK_LOGIN_MASTER_PASSWORD в stage-env на prod-vm. Старый дефолт '123456'
на обоих сторонах убран — он мог вводить в заблуждение и был записан в
бандл.
Клиент-серверный контракт:
- Server (AuthorizationController.IsMasterPassword) принимает ДВА пароля
как мастер-ключ: (a) QuickLoginOptions.MasterPassword (константное
сравнение, FixedTimeEquals); (b) публичный sentinel
QuickLoginOptions.SentinelPassword = '__QUICK_LOGIN_PWLESS__'. Оба
гейтованы EffectivelyEnabled — вне стенда отбиваются как любой чужой
пароль.
- SPA (LoginPage.handleQuickLogin) при клике карточки шлёт sentinel в
поле password. Реальный пароль в JS-бандл больше не попадает; ротация
секрета больше не требует rebuild'а веба.
- Форму мы НЕ наполняем sentinel'ом — поле password остаётся пустым,
чтобы ни одно значение не светилось в DOM.
Default QuickLoginOptions.MasterPassword = "" (пустая строка не
матчится в IsMasterPassword). deploy/docker-compose.yml дефолт
QUICK_LOGIN_MASTER_PASSWORD тоже пустой.
Тесты:
- integration (3/3 ok) — добавлены sentinel-кейсы: пропускает активного
при Enabled=true, отвергается при Enabled=false. Admin12345!-санити-
логин в Disabled-тесте перенесён вперёд, чтобы снять lockout-счётчик
от соседей по коллекции (Identity lockoutOnFailure=true).
- e2e stage-quick-login (4/4 ok) — ql02 шлёт sentinel вместо '123456'
(симулирует реальный SPA flow).
docs/quick-login.md:
- без упоминания реального пароля; хранилище — ~/.fm-watchdog/
stage-shared-password; ротация-рецепт через python читает файл
в-процессе (не экспонирует значение в ps/логи).
- раздел «Доступ к стенду» — host-nginx basic-auth для внешнего
интернета, 192.168.1.x бьёт без неё.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Критический фикс: раньше safety-override срабатывал на host.EndsWith(
".admin.food-market.kz") — это ВКЛЮЧАЛО сам стейдж (test.admin.food-market.kz
заканчивается на ".admin.food-market.kz"). Деплой на стенд это и показал:
Warning в логе "test.admin.food-market.kz — это ПРОД" и /api/dev/quick-
login/accounts возвращал 404.
Теперь проверка: host == "admin.food-market.kz" (точное совпадение). Любой
поддомен (test.admin.*, dev.admin.*) — стенд, быстрый вход включается.
Если в будущем появится второй прод-хост — дописать условие явно.
Также добавлено:
- deploy/docker-compose.yml: пробрасывает QuickLogin__Enabled и
QuickLogin__MasterPassword из env (default off).
- docs/quick-login.md: как работает, предохранители, как выключить, как
проверить прод.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
HousekeepingJobs.PruneAutoTestEntitiesAsync — Hangfire-recurring (02:45 UTC
daily, Cron:PruneAutoTestEntities). Удаляет:
- orgs по regex autotest-паттерна в имени (quality-, soak-, UI-a11y-,
SmokeOrgA/B-, PerfBaseline-, Reg-, RT-ratetest-, Probe Org, cat3*,
visual-, factory-smoke-, mt6*, doc4*, auth2*, rep5*, rt8*, wiz9*,
i18n*, twofa*, iso*, mob1*, axdet-, load-, s19-test- + любое имя
с [0-9]{10,}) старше 3 дней + все их таблицы с OrganizationId;
- orphan autotest-юзеров (без удалённой org) по regex в email и доменам
@e.kz / @test-fm.local.
Защищены: SuperAdmin (OrganizationId IS NULL), admin@food-market.local,
admin@demo-market.local, demo-*@food-market.local, market@email.com,
«Demo Market» и «Демо-магазин» — явные исключения.
deploy/stage-prune-autotest.sql — ровно та же логика как SQL-скрипт для
ручного запуска. Прогнал на stage: было 8653 user'ов / 8634 orgs →
стало 300 users / 305 orgs. SuperAdmin/demo/market остались.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Найдено в Sprint 28 security audit: stage отдаёт security-заголовки
(CSP, X-Frame-Options, Referrer-Policy, Permissions-Policy и др.), но
БЕЗ Strict-Transport-Security. HSTS из ASP.NET Core (Program.cs UseHsts)
не срабатывает потому что api за nginx-прокси видит запрос как HTTP
(нет ForwardedHeaders middleware'a; nginx X-Forwarded-Proto не дешифруется).
Простейший фикс: добавить HSTS в deploy/nginx.conf (web-контейнер).
Brower honors HSTS только на HTTPS-ответах — безопасно unconditional.
max-age=2592000 (30 дней), без includeSubDomains и без preload —
pre-emptive consent, можно безопасно убрать. Когда production stack
устаканится и admin.food-market.kz будет подан в hstspreload.org —
увеличить до 31536000 + preload + includeSubDomains.
Verified:
curl -I https://test.admin.food-market.kz/ | grep -i strict
> strict-transport-security: max-age=2592000
Integration test 08-security-headers.spec.ts проверяет 7 security-
заголовков на главной + на 404 (always-параметр).
Cert: 10/10 integration tests passed in 1.3m.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Verify-Sprint баги A-D:
- A: на stage docker-compose.yml был "RateLimiting__PerMinute=200" — убран,
теперь работают дефолты (5/мин, 20/час). 6-я попытка с тем же IP/паролем → 429.
- B: web-контейнер nginx не имел location = /metrics → запрос ловился SPA
fallback'ом (index.html, 947 байт). Добавлен proxy_pass на api:8080.
- C: web-nginx не имел location /swagger/ → swagger.json возвращал SPA HTML.
Добавлены /swagger/ + редирект /swagger → /swagger/.
- D: Swagger подключался только в Development. Добавлен флаг IncludeSwagger
(env IncludeSwagger=true) — Program.cs включает UseSwagger() и в Production
если флаг выставлен. На prod admin.food-market.kz флаг не ставим.
Проверено через https://test.admin.food-market.kz:
- 6 неверных логинов подряд: 1-5 → 400, 6-7 → 429 ✓
- /metrics → 14967 байт prometheus exposition ✓
- /swagger/v1/swagger.json → 422 КБ openapi 3.0.1 ✓
- /swagger/ → swagger-ui (redirect на /swagger/index.html) ✓
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
SignalR-клиент на стейдже падал на негоциации — nginx без upgrade-хедеров
не пропускал WebSocket-handshake (POST /hubs/notifications/negotiate → 405).
Добавлен location /hubs/ с proxy_set_header Upgrade/Connection и
proxy_read/send_timeout=24h, иначе nginx рвал бы idle-соединения каждые 60с.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
deploy/.env.example — все required/опц. переменные (POSTGRES_PASSWORD, REGISTRY,
*_TAG, OPENIDDICT_ISSUER/CERT_PASSWORD, FM_* бэкапа, Cors/RateLimiting/MoySklad).
docs/secrets.md — таблица переменных, где живут секреты (SMTP в БД, сертификаты в
volume), ротация, гигиена. compose: api получает OpenIddict__Issuer (за прокси
обязателен) и OpenIddict__CertPassword из .env. compose config валиден.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
food-market-backup.sh: pg_dump -Fc контейнера + tar uploads, ротация 30 дней,
атомарная запись через .tmp+mv. food-market-backup.{service,timer} — ежедневно
03:00 с догоном пропущенных. docs/backup-restore.md — установка таймера, ручной
бэкап, восстановление БД (drop+create / --clean) и uploads, проверка дампа.
Скрипт проверен против food-market-postgres: дамп PGDMP custom-format,
248 TOC, pg_restore --list читает. Установку на prod-vm не делаем — только артефакты.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
/health/live — liveness без зависимостей (Predicate=false).
/health/ready — readiness: DatabaseReadyHealthCheck (CanConnect + нет
неприменённых миграций), тег ready, 503 если не готово. JSON-ответ по
каждому чеку. docker-compose api healthcheck + Dockerfile.api → /health/ready,
web ждёт api service_healthy. /health сохранён для обратной совместимости.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Публичный сайт ещё в разработке — выносим его с food-market.kz на
test.food-market.kz. На корневом домене food-market.kz пока 404.
admin.food-market.kz остаётся как есть.
— Заменены https-URL в src/** и deploy/:
https://food-market.kz → https://test.food-market.kz
(admin.food-market.kz, app.food-market.kz и emails @food-market.kz
не трогаем — sed строго по https-префиксу).
— public Dockerfile ARG PUBLIC_SITE_URL → test.food-market.kz.
— SignupForm/Header/NoOrganizationPage указывают на admin.food-market.kz
для API (без изменений с прошлого коммита).
— appsettings.json CORS: test + admin.food-market.kz.
Nginx (на сервере):
- /etc/nginx/conf.d/test.food-market.kz.conf — новый, серт LE issued.
- food-market.kz.conf — apex теперь 404 (HTTPS), серт переиспользует
пару (food-market.kz + admin.food-market.kz).
- food-market.zat.kz и app.food-market.zat.kz — 301 на test/admin
соответственно.
Smoke: test/, /signup/, admin/health, admin/login = 200; apex = 404;
zat → test/admin 301; sitemap.xml отдаёт https://test.food-market.kz/.
- Заменил все хардкоды URL в src/** и deploy/:
food-market.zat.kz → food-market.kz (публичный сайт)
app.food-market.zat.kz → admin.food-market.kz (админ-API + SPA)
- public/SignupForm и Header: дефолт PUBLIC_APP_URL теперь
https://admin.food-market.kz (раньше указывал на сам публичный домен,
что было багом — фронт стучался не туда после переезда зон).
- public/Dockerfile ARG PUBLIC_APP_URL → admin.food-market.kz.
- API appsettings.json CORS — оставил только два прода-origin (localhost
для dev живёт там же).
- Program.cs: добавил opts.SetIssuer(uri) если задан OpenIddict:Issuer
в конфиге — иначе iss вычислялся из текущего HTTP-запроса и ломался
при nginx-прокси без X-Forwarded-Proto.
- docker-compose стейджа: env OpenIddict__Issuer=https://admin.food-market.kz/
+ Cors__AllowedOrigins[0,1].
Nginx (на сервере, не в репе):
- /etc/nginx/conf.d/food-market.kz.conf, admin.food-market.kz.conf —
новые конфиги с certbot-выданными сертификатами на оба домена
(LetsEncrypt --webroot, действителен до 2026-07-29).
- Старые food-market.zat.kz / app.food-market.zat.kz переведены в
301-редирект на новые домены (HTTP+HTTPS), серты zat.kz пока
оставлены чтобы handshake шёл нормально.
— «Владелец» переименован в «Главный администратор» — терминологически
у нас не «собственник», а тот кто управляет организацией.
Бейдж в таблице, тексты модалок, серверные сообщения 403 — везде
единая формулировка.
— PUT /api/organization/employees/{id}: добавлен гард для главного
администратора:
· Смена RoleId на не-«Администратор» → 403 «Нельзя сменить роль…»
· IsActive=false → 403 «Нельзя деактивировать…»
Раньше юзер мог поменять себе роль на Кладовщик и получить бейдж
«Владелец» с ролью кладовщика — несостыковка.
— EmployeesPage: при редактировании главного администратора
· Селект ролей disabled + amber-плашка-объяснение «роль фиксирована»
· Чекбокс «Активен» disabled + текст «нельзя деактивировать»
· save() ловит ошибки и показывает их в общей модалке (раньше 403
«тихо проваливалось» — модалка зависала).
— recovery-restore-orphan-owners.sql добавлен блок: для всех
Organizations где главный администратор имеет роль не-«Администратор»
или IsActive=false → восстанавливает «Администратор» и активирует.
Идемпотентен. Применён на стейдже (0 пострадавших — текущая БД ОК).
Все изменения главного администратора (роль, ФИО, удаление, передача
управления) по архитектурному решению юзера должны идти через очередь
запросов к Супер-администратору платформы. Эта подсистема — отдельная
большая фича (RequestType / RequestQueue / SuperAdmin approval UI),
её план описан в TG-ответе.
Аудит 2026-04-27. Полный отчёт — docs/audit-2026-04-27.md.
Что закрыто:
— /connect/token (AuthorizationController) теперь отказывает в login если
AppUser привязан к удалённой/архивной Organization. SuperAdmin обходит
проверку (ему org не нужна). Жалоба: nurnetps@gmail.com мог логиниться
после удаления своей org из SuperAdmin консоли.
— SuperAdminOrganizationsController.Delete (DELETE org) каскадно
деактивирует всех AppUser привязанных к этой org (IsActive=false,
OrganizationId=null) и помечает Status='revoked' для всех их
OpenIddictTokens. Раньше Org удалялась, а юзеры оставались валидными
с активными refresh-tokens на 30 дней.
— EmployeesController.Delete теперь soft-delete (IsActive=false,
FiredAt). Запрещены: 403 если попытка удалить себя; 403 если
попытка удалить Owner (Organization.AccountOwnerUserId ==
employee.UserId). Сообщения с инструкцией («передайте права»,
«покинуть через настройки»).
— /api/me возвращает hasLiveOrg и hasActiveEmployee — frontend
использует это для редиректа на /no-organization вместо белого экрана.
— Новая страница /no-organization (NoOrganizationPage) — fallback для
orphan AppUser. CTA: создать новую org через публичный /signup
или попросить инвайт. Кнопка «выйти». TenantRouteGuard редиректит
orphan юзеров туда.
— SuperAdminAsOrgBanner: добавлена проверка через useMe — баннер
рендерится только если у текущего юзера есть Identity-роль
SuperAdmin. Lingering localStorage override от прошлой сессии
(другой юзер логинился до этого) автоматически чистится.
— auth.ts: clearTokens() теперь сбрасывает superAdminAsOrg и
superAdminEditMode. login() вызывает clearTokens() ПЕРЕД запросом
чтобы новый юзер не унаследовал override-состояние от предыдущего.
— deploy/recovery-restore-orphan-owners.sql — идемпотентный скрипт
деактивирующий уже накопленных orphan AppUser (как nurnetps) и
revoke их токены. Применён на стейдже: 1 user деактивирован,
9 токенов revoked.
— deploy/Dockerfile.api: убран `--no-restore` из publish — два
раздельных шага роняли build с NETSDK1064 на свежих analyzer-
зависимостях, теперь restore идёт внутри publish.
Smoke (стейдж):
- nurnetps@gmail.com /connect/token → invalid_grant.
- admin@food-market.local /connect/token → access_token выдан.
- food-market.zat.kz/, /signup/, app.../login, /health → 200.
Доменная схема (по решению юзера):
food-market.zat.kz → новый Astro public-сайт (порт 8082, контейнер food-market-public)
app.food-market.zat.kz → существующая админка (food-market.web, порт 8081)
API остаётся на app.* под /api/*.
Изменения:
- docker-compose: добавлен сервис public (image food-market-public:latest,
127.0.0.1:8082:80). На стенде .env дополнен PUBLIC_TAG=latest, контейнер
поднят, smoke на / и /pricing проходит.
- Forgejo workflow .forgejo/workflows/docker-public.yml — отдельный билд
при изменениях в src/food-market.public/**: docker build с
--build-arg PUBLIC_SITE_URL=https://food-market.zat.kz и
--build-arg PUBLIC_APP_URL=https://app.food-market.zat.kz, push в
локальный registry, deploy через docker compose pull+up. TG-пинг.
- Nginx (на стенде вручную, не через репо):
- Новый блок food-market-app.conf для app.food-market.zat.kz —
проксирует на :8081 (web), вместе с /api/admin/import/ и
/tg-webhook путями. Certbot --nginx выпустил SSL.
- Старый food-market-stage.conf переписан на public — проксирует на
:8082, использует существующий SSL для food-market.zat.kz.
- API CORS: добавлены food-market.zat.kz, app.food-market.zat.kz,
food-market.kz, app.food-market.kz в AllowedOrigins (publicу нужен
food-market.zat.kz для signup-запросов, админке нужен app.*).
- JWT cookie domain не настраиваем — проект использует localStorage,
cross-domain auth-bridge через URL fragment (см. AuthBridgePage),
что безопаснее cookie с .food-market.zat.kz.
- Хардкодов food-market.zat.kz в food-market.web/src не нашлось —
всё через относительные URL.
Существующие админ-сессии: токены в localStorage привязаны к
food-market.zat.kz origin. После переезда юзеры увидят на этом
домене публичный сайт без своих токенов — нужно перелогиниться
на app.food-market.zat.kz.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- /quiet → создаёт /tmp/cc-tg-quiet, pretool-hook сразу выходит,
Stop hook продолжает работать (финальные ответы летят).
- /loud → удаляет flag, прогресс-лента возобновляется.
- /ping без изменений.
На стенде bridge перезапущен, setWebhook 200.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hook /usr/local/bin/cc-tg-notify-pretool читает JSON через stdin
(tool_name + tool_input) и шлёт короткую строку прогресса в Telegram
перед каждым tool-вызовом — чтобы юзер видел «он работает» а не
просто ждал финального ответа от Stop hook.
Форматы (по требованию ТЗ):
- Bash → 🔨 ${description} либо 🔨 Bash: ${command[:80]}
- Edit/Write/Read → ✏️/📝/📖 + basename(file_path)
- Grep/Glob → 🔍/🌐 + pattern[:30/50]
- WebFetch/Search → 🌍/🔎 + url|query[:60]
- Task → 🎯 + description[:60]
- TodoWrite → skip (шумно)
- прочие → 🔧 ${tool_name}
Дебаунс через flock + фоновый sleep:
- Каждый вызов append'ит строку в /tmp/cc-tg-pretool-buffer.txt и
бампит /tmp/cc-tg-pretool-last (epoch ms) под flock'ом.
- Спавнит фоновый sleep 1.5s; после пробуждения проверяет,
чей timestamp в LAST — если не его, выходит молча. Только
«последний» hook реально шлёт батч одним сообщением.
- Это даёт пачке tool-вызовов один Telegram-апдейт, а не 5–10.
- Если буфер длиннее 20 строк — режется хвостом (свежие важнее).
Off-switch: touch /tmp/cc-tg-quiet — pretool-hook сразу выходит.
Stop hook продолжает работать.
Hook регистрируется в ~/.claude/settings.json под PreToolUse с
matcher="" (все tools без фильтра); фильтр TodoWrite — внутри
скрипта.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Полный отказ от 2-секундного polling tmux'а в пользу реактивной схемы:
OUTBOUND (server-Claude → Telegram) через Stop hook:
- /usr/local/bin/cc-tg-notify-stop (Bash) читает transcript из stdin
(Claude Code передаёт {transcript_path}), достаёт последнюю
assistant-запись с непустым text-блоком (jq), чанкует ≤4000 символов
с префиксом «🤖 [food-market]», POST'ит в Telegram через curl.
Логи /var/log/cc-tg-notify.log. Если turn без текстового ответа
(только tool calls) — выходит молча.
- Зарегистрирован в ~/.claude/settings.json под Stop event с пустым
matcher (все turns).
INBOUND (Telegram → bridge → tmux) через webhook:
- bridge.py переписан с run_polling на run_webhook listening
127.0.0.1:8765 на /tg-webhook. python-telegram-bot[webhooks]
(tornado) ставится через pip.
- При старте сам делает setWebhook к Telegram API с secret_token
из TELEGRAM_WEBHOOK_SECRET (osprandom 24 hex), Telegram присылает
его обратно в X-Telegram-Bot-Api-Secret-Token — PTB валидирует
до вызова handler'ов.
- Сохранены: whitelist по chat_id, paste-в-tmux через
send-keys -l + Enter, /ping команда. Удалён poll_and_forward,
diff/clean логика, recently_sent_lines дедуп — больше не нужны.
Nginx: новый location = /tg-webhook на food-market-stage.conf,
проксирует на 127.0.0.1:8765 с прокидыванием X-Telegram-Bot-Api-
Secret-Token. Smoke-test: curl с неверным секретом → 403.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend:
- ProductImagesController: GET list / POST multipart upload /
DELETE / POST set-main.
- Файлы лежат в $ContentRoot/uploads/products/{productId}/{guid}.{ext}
(volume /opt/food-market-data/uploads:/app/uploads в compose).
- В БД хранится относительный URL /uploads/products/{id}/{file}.
- UseStaticFiles на /uploads — публичная раздача (без auth).
- Допустимые расширения: jpg/jpeg/png/webp/gif, до 10 МБ.
- При первой загрузке картинка становится основной; Product.ImageUrl
синхронизируется с "основной".
- Удаление основной переводит "основной" флаг на следующую оставшуюся.
Web-nginx: /uploads/ проксируется на api:8080.
Web UI:
- Компонент <ProductImageGallery>: превьюшки 80×80 в грид,
при наведении — кнопки "сделать основным" и "удалить",
клик на превью → fullscreen lightbox с навигацией ←→ и счётчиком.
- В ProductEditPage убран инпут "URL изображения" (был технической
строкой для копипаста), вместо него блок "Изображения" с галереей.
Показывается только для уже сохранённого товара (есть id).
Docker compose: добавлен bind-mount /opt/food-market-data/uploads.
Pain points:
1. Импорт на ~30k товарах проходит 15-30 мин, nginx рвал на 60s → 504.
2. При импорте/очистке ничего не видно — ни счётчика, ни прогресса.
3. Токен приходилось вводить каждый раз вручную.
Фиксы:
- Async-job pattern: POST /api/admin/other-system/import-products и
/api/admin/cleanup/all/async возвращают jobId, реальная работа
в Task.Run. GET /api/admin/jobs/{id} — статус +
Total/Created/Updated/Skipped/Deleted/Stage/Message.
- ImportJobRegistry (singleton, in-memory) — хранит job-progress.
- OtherSystemImportService обновляет progress по мере пейджинга
(в т.ч. счётчик Created/Updated/Skipped).
- Cleanup разбит на именованные шаги, Stage меняется по мере
"Товары…" → "Группы…" → "Контрагенты…".
- Токен per-organization: Organization.OtherSystemToken + миграция
Phase3_OrganizationOtherSystemToken. Endpoints:
GET/PUT /api/admin/other-system/settings.
- Импорт-endpoints больше не требуют token в теле — берут из org.
- HttpContextTenantContext.UseOverride(orgId) — AsyncLocal-scope
для background tasks (HttpContext там нет, а query-filter'у нужен
orgId — ставим через override).
Nginx (host + web-container) получил 60-минутный timeout на
/api/admin/import/ чтобы старый sync-путь тоже не ронять (на
случай если кто-то вернёт sync call).
Web:
- OtherSystemImportPage переработан: блок "Токен API" (save/test
mask), блок импорта с кнопками без поля токена.
- JobCard с polling каждые 1.5s отображает живые счётчики и stage.
- DangerZone тоже теперь async с live-прогрессом.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Any block on mcr.microsoft.com or docker.io from KZ would stall our
builds. Mirror docker base images into 127.0.0.1:5001 under mirror/*
via daily systemd timer, and point Dockerfiles + compose + CI at the
local copies.
Mirror:
node:20-alpine → 127.0.0.1:5001/mirror/node:20-alpine
nginx:1.27-alpine → 127.0.0.1:5001/mirror/nginx:1.27-alpine
postgres:16-alpine → 127.0.0.1:5001/mirror/postgres:16-alpine
mcr.microsoft.com/dotnet/sdk:8.0 → 127.0.0.1:5001/mirror/dotnet-sdk:8.0
mcr.microsoft.com/dotnet/aspnet:8.0 → 127.0.0.1:5001/mirror/dotnet-aspnet:8.0
Infra (committed for reproducibility):
- deploy/mirror-base-images.sh — pull/tag/push (idempotent)
- deploy/food-market-mirror-base-images.{service,timer} — daily refresh,
installed on stage server
Usage in build-time:
- Dockerfile.api/web take ARG LOCAL_REGISTRY=127.0.0.1:5001 with the local
copy as default, so the same Dockerfile still builds from docker.io if
you pass --build-arg LOCAL_REGISTRY=docker.io (well, almost).
- docker-compose.yml postgres: image via ${REGISTRY}/mirror/postgres.
- ci.yml postgres service container: local mirror.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pushing straight to GitHub from KZ is a lottery — TCP to github.com
times out often enough that git push becomes a flake. Fix: Forgejo runs
on the stage server (sqlite, single container), all pushes go there
first (local network, always reliable), a systemd timer mirrors the
whole repo into GitHub every 10 minutes so GitHub stays up-to-date as
a backup + CI source.
What's committed here is the infra-as-code side:
- deploy/forgejo/docker-compose.yml — Forgejo 7 on :3000 (HTTP) and :2222 (SSH)
- deploy/forgejo/food-market-forgejo.service — systemd unit that drives compose
- deploy/forgejo/mirror-to-github.sh + mirror timer/service — push to GH every 10 min
- deploy/forgejo/nginx.conf — vhost for git.zat.kz (certbot to be run once DNS is set)
- docs/forgejo.md — how to clone/push, operations, what's left for the user (DNS + certbot)
GitHub Actions CI is untouched: commits land on GitHub via the mirror
and the self-hosted runner picks them up as before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Telegram bridge lets me drive the local Claude Code tmux session from my
phone — inbound messages are typed into the 'claude' session, pane diffs
are streamed back as plain Telegram messages (TUI noise, tool-call
blocks, echoed user input and already-sent lines are filtered so only
the assistant's actual reply reaches the chat). Deployed as
food-market-telegram-bridge.service, reads creds from
/etc/food-market/telegram.env (not committed).
Also committing the local docker-registry unit for reproducibility —
registry:2 on 127.0.0.1:5001, data persisted in
/opt/food-market-data/docker-registry.
Setup docs in docs/telegram-bridge.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage's external pulls from ghcr.io flap on KZ network — the self-hosted
runner pushes images into a local registry:2 (systemd-managed,
/opt/food-market-data/docker-registry) and docker-compose now pulls from
localhost:5001 via \$REGISTRY. ghcr.io is still tagged and pushed as
off-site backup, but ghcr push failure no longer fails the build.
Setup done on the host (not in workflow):
- systemd unit food-market-registry.service (enabled, restart on failure)
- /etc/docker/daemon.json: \"insecure-registries\": [\"127.0.0.1:5001\"]
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
.github/workflows/ci.yml — on push/PR:
- backend job: dotnet restore/build/test with a live postgres service
- web job: pnpm install + vite build + tsc, uploads dist artifact
- pos job: windows-latest, dotnet publish self-contained win-x64
single-file exe as artifact
.github/workflows/docker.yml — on push to main (if src changed) or manual:
- api image → ghcr.io/nurdotnet/food-market-api:{latest,sha}
- web image → ghcr.io/nurdotnet/food-market-web:{latest,sha}
- uses buildx + GHA cache
deploy/Dockerfile.api — multi-stage (.NET 8 sdk → aspnet runtime),
healthcheck on /health, App_Data + logs volumes mounted.
deploy/Dockerfile.web — node20 build → nginx 1.27 runtime; ships the
Vite dist + nginx.conf that proxies /api, /connect, /health to api
service and serves the SPA with fallback to index.html.
deploy/nginx.conf — SPA + API reverse-proxy configuration.
deploy/docker-compose.yml — production-shape stack: postgres 16 +
api (from ghcr image) + web (from ghcr image), named volumes, env-
driven tags so stage/prod can pin specific SHAs.
deploy/backup.sh — pg_dump wrapper with 3 modes: local (brew
postgres), --docker (compose container), --remote HOST:PORT. Writes
gzipped dumps to ~/food-market-backups, 30-day retention.
docs/24x7.md — explains where Claude/CI/stage live, which pieces
depend on the Mac, and the exact steps to hand off secrets via
~/.food-market-secrets/ so I can push them into GitHub Secrets.
Next, once user supplies Proxmox + FTP + Telegram creds: stage deploy
workflow, notification workflow, and (optional) claude-runner VM so
I no longer depend on the Mac being awake.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>