# Sprint 20 — Mapster + SSO scaffolding + maintenance automation Цель: закрыть TD-3 (Mapster вместо ручных LINQ-проекций), добавить SSO-скелет (Google + Microsoft), включить maintenance-автоматику (stale cleanup / VACUUM / disk / performance regression / analytics). Старт: 2026-06-07 (после Sprint 19). Исполнитель: Claude Opus 4.7. ## Принципы - Mapster — без AutoMapper (платный + CVE), config в `Application/Mapping/`. - SSO — только скелет. Реальные client_id/secret не коммитим, пустые → 503. - Hangfire jobs — идемпотентные, с лимитом на rows (не зачищать слишком много за раз). - НЕ трогать: `global.json`, prod admin.food-market.kz, POS WPF. ## Чек-лист - [x] **1. TD-3 Mapster** — `Application/Mapping/MapsterConfig.cs` с `TypeAdapterConfig` для Product+ProductBarcode+ProductPrice+Counterparty. Singleton зарегистрирован в `Program.cs`. ProductsController.List/Get/ GetInternalAsync + CounterpartiesController.List/Get переведены на `.ProjectToType(MapsterConfig.Config)`. Inline `Projection` удалён. - [x] **2. SSO Google + Microsoft scaffolding** — пакеты `Microsoft.AspNetCore.Authentication.Google` 8.0.11 + `.MicrosoftAccount` 8.0.11 + `.Cookies` 2.3.0. Условная регистрация в Program.cs: если `Authentication:{Google|Microsoft}:ClientId` пустой — провайдер не подключается. `ExternalAuthController` с endpoint'ами: - `GET /api/auth/external/{provider}` — Challenge или 503 с подсказкой - `GET /api/auth/external/callback?provider=...` — 501 с email (invite-flow TODO) - `GET /api/auth/external/providers` — `{google: bool, microsoft: bool}` - `docs/sso.md` — инструкция получения keys у Google/Microsoft. - [x] **3. Stale-data cleanup автоматика** — HousekeepingJobs расширен: - `PruneOrgAuditLogAsync` — `OrgAuditLog` > `Cleanup:OrgAuditLogDays` (90) - `PruneDraftsAsync` — Supply/RetailSale/Demand в Draft > `Cleanup:DraftDays` (30) - `PruneRevokedRefreshTokensAsync` — `OpenIddictTokens` Type=refresh, Status=revoked/redeemed > `Cleanup:RevokedRefreshTokenDays` (7). Три новых cron'a в `HangfireJobsConfigurator` (03:00 / 03:15 / 03:20 UTC). - [x] **4. DB VACUUM automation** — `DatabaseMaintenanceJobs.VacuumTopTablesAsync`: `pg_total_relation_size` выбирает топ-`Maintenance:VacuumTopN` (5) таблиц → `VACUUM (ANALYZE) public.""` per-table. Без `FULL` → не блокирует пишущие транзакции. Логирует время per-table. Cron еженедельно вс 04:00 UTC (`Hangfire:Cron:VacuumTopTables`). - [x] **5. Disk usage monitoring** — `DiskMonitoringJob` ежечасно (`Hangfire:Cron:DiskMonitor`): `DriveInfo.AvailableFreeSpace` на пути из `Monitoring:DiskPaths` (default `/opt,/var/lib/docker`). При свободе < `Monitoring:DiskMinFreeBytes` (1GB) → Telegram-alert на `Monitoring:SuperAdminTelegramChatIds` (CSV). Anti-spam: один alert per mount per `Monitoring:DiskAlertCooldownHours` (6) часов (in-memory). Prometheus-gauge `food_market_disk_free_bytes{mount="..."}` обновляется каждым прогоном. - [x] **6. Performance regression detection** — `~/nightly-perf-check.sh`: парсит `/metrics` stage'а, считает `db_avg_ms = sum/count` по `food_market_db_query_duration_seconds`, сравнивает с baseline в `~/.fm-watchdog/perf-baseline.json`. Δ>`PERF_THRESHOLD_PCT` (30%) → Telegram-alert. Sliding window: baseline обновляется только при «нет регрессии». Cron-устанавливаемый скрипт (запускать после `nightly-verify.sh`). - [x] **7. Public-site analytics placeholder** — Astro `food-market.public/src/layouts/BaseLayout.astro` рендерит GA4 `