From b4d332ad41f52a450fa6aa707017bf3ca9e36a68 Mon Sep 17 00:00:00 2001 From: barach6662001-bit Date: Fri, 24 Apr 2026 07:54:22 +0000 Subject: [PATCH] docs: add ROADMAP.md, TZ.md, and AGENT_QUICKSTART.md for agent handoff Captures completed work across PR #602-611, remaining scope across PR #612-617, locked architectural decisions, technical debt, and agent protocol. --- docs/AGENT_QUICKSTART.md | 76 ++++++++++++ docs/ROADMAP.md | 178 ++++++++++++++++++++++++++++ docs/TZ.md | 246 +++++++++++++++++++++++++++++++++++++++ docs/plan.md | 58 ++++++++- 4 files changed, 554 insertions(+), 4 deletions(-) create mode 100644 docs/AGENT_QUICKSTART.md create mode 100644 docs/ROADMAP.md create mode 100644 docs/TZ.md diff --git a/docs/AGENT_QUICKSTART.md b/docs/AGENT_QUICKSTART.md new file mode 100644 index 00000000..4af9bda0 --- /dev/null +++ b/docs/AGENT_QUICKSTART.md @@ -0,0 +1,76 @@ +# Agent Quickstart + +Short reference for starting any PR. Copy-paste to the agent when launching a new session. + +--- + +## Standard command (use for every new PR) + +``` +Read docs/ROADMAP.md and docs/TZ.md. + +Verify git log --oneline -20 on main matches the "Completed" section of ROADMAP.md. + +Start the PR listed under "In progress" in ROADMAP.md. + +Rules: +- Do not redo closed work (check status markers in TZ.md). +- Do not skip ahead to later PRs. +- Do not propose scope outside the current PR. +- Decisions listed in ROADMAP.md "Decisions locked" section are final — do not re-discuss. +- If ROADMAP and codebase disagree, stop and ask in chat — do not guess. +- .mcp.json is gitignored, do not commit it. + +Workflow per PR: +1. Create a feature branch from main +2. Implement scope incrementally, commit each logical step with a clear message +3. Run `dotnet test` + `npx tsc --noEmit` before each commit +4. Open PR with description linking to ROADMAP item and affected TZ points +5. Fix CI failures as needed +6. Squash-merge to main +7. Update ROADMAP.md: move completed item to "Completed", promote next from "Upcoming" to "In progress" +8. Update TZ.md status markers for affected points +9. Commit the ROADMAP/TZ update as first commit of next PR OR as a final chore commit + +Begin. +``` + +--- + +## When things go wrong + +**Agent proposes already-shipped work:** +> Check ROADMAP.md "Completed" section again. Your proposed work is in PR #X. Re-read main git log and restart planning. + +**Agent proposes scope creep:** +> That is out of the current PR scope. Current PR is defined in ROADMAP.md "In progress" section only. If you think the extra scope is critical, stop and ask in chat. + +**Agent is unsure about a design decision:** +> Check ROADMAP.md "Decisions locked" section. If your question is answered there, proceed. If not, ask in chat before proceeding. + +**Agent wants to defer tests:** +> Tests are not deferrable for security-critical or data-integrity code. If the test infrastructure is missing, extend it in this PR, not "later". See PR #610 and #611 — test debt was created and had to be immediately closed in a follow-up, that's a pattern to avoid. + +**CI fails repeatedly on same issue:** +> Stop iterating. Report the failure in chat with the exact error. Three failed iterations on the same issue means the approach is wrong, not that one more tweak will fix it. + +--- + +## When a new TZ point emerges mid-roadmap + +During roadmap execution, new bugs or requirements will come up. Protocol: + +1. Do not interrupt the current PR. +2. Add the new item to TZ.md as a new point with `[NOT YET SCHEDULED]` marker. +3. Add to ROADMAP.md under "Technical debt" or a new "Emerging scope" section. +4. At the next PR boundary, discuss priority in chat before deciding if it slots into upcoming PRs or waits. + +--- + +## Keeping ROADMAP accurate + +After each merge the roadmap MUST reflect reality. Common drift patterns to avoid: + +- Agent marks a PR "complete" in ROADMAP while leaving part of scope as "deferred". Either the scope is shipped or it's not. Deferred scope stays in "In progress" as a follow-up or moves to "Upcoming" as a new PR. +- Agent updates "Completed" but forgets to promote next item into "In progress". Always do both. +- Agent edits plan inline in chat instead of the file. File is source of truth. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 00000000..033e1e03 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,178 @@ +# AgroPlatform Roadmap + +> Living document. Agent updates this after each PR merges. +> For context on individual TZ points, see `docs/TZ.md`. +> For locked architectural decisions, see "Decisions locked" section below. + +--- + +## Completed + +- [x] **PR #602** — honest dashboard, removed demo margin fake, Season = all-time +- [x] **PR #603** — removed non-functional Plan/Fact card from /expenses *(TZ 7)* +- [x] **PR #604** — root route `/` → landing page, forced dark-only theme *(TZ 1, 8.1)* +- [x] **PR #605** — dashboard period URL state (`?period=`), resolved date range label, drill-down to Costs *(TZ 2 partial, TZ 3 for costs)* +- [x] **PR #606** — monthly revenue chart drill-down, SalesList URL-state *(TZ 3 for revenue)* +- [x] **PR #607** — `/api/tenant/data-boundaries`, `‹ ›` arrow stepping with keyboard, `` shared component *(TZ 2 partial, TZ 6 partial)* +- [x] **PR #609** — tenant feature flags, `[RequireFeatureFlag]` middleware, `FeatureFlagGate`, Budget hidden by flag *(TZ 4, TZ 5)* +- [x] **PR #610** — super-admin foundation: `IsSuperAdmin`, JWT claims (`is_super_admin`, `mfa_verified`), TOTP MFA, `AdminController` with `IgnoreQueryFilters()`, `/admin/tenants` + `/admin/tenants/:id` pages, audit log table *(TZ 14 foundation)* +- [x] **PR #611** — super-admin integration tests (8 scenarios), `TestAuthHandler` extended with opt-in headers *(test debt from #610 closed)* + +--- + +## In progress + +- [ ] **PR #612 — Full Season model** *(TZ 2 remainder)* + - Replace hardcoded year-list with `Seasons` table (Id, TenantId, Code, Name, StartDate, EndDate, IsCurrent) + - Data migration: seed 3 default seasons per existing tenant (2023/24, 2024/25, 2025/26) + - CRUD for super-admin (`/api/admin/tenants/{id}/seasons/*`) and tenant-admin (`/api/seasons/*`) + - Dashboard: season arrows iterate real Season list, label uses `StartDate/EndDate` + - Unique constraint: only one `IsCurrent=true` per tenant (partial index) + - Breaking change to `/api/seasons` response shape — update all frontend consumers in same PR + +--- + +## Upcoming (in order — do not reorder without approval) + +- [ ] **PR #613 — Currency system** *(TZ 8.2)* + - `UserPreferences.PreferredCurrency` (UAH/USD/EUR, default UAH) + - `ExchangeRate` table (Code, Date, RateToUah), PK (Code, Date) + - `NbuCurrencyService` + cron 06:00 Kyiv, backfill from 2024-01-01 + - `useFormatCurrency()` hook, settings UI in Профіль → Валюта + - Fallback: last stored rate on NBU failure; weekend/holiday → previous business day + - Exports: currency header with NBU rate on export date + +- [ ] **PR #614 — Super-admin advanced + impersonation** *(TZ 14 remainder)* + - Impersonation: 60min TTL, mandatory reason, red banner in UI, email to target user, rate limit 3/day per (admin, target) pair + - Forbidden actions in impersonation: password/email change, API keys write scope, billing ops, tenant export + - `/admin/users` global search, impersonate action + - `/admin/audit-log` global view with filters (tenant, user, action type, period) + - `/admin/system` (queue/jobs health, storage, connections) + - `/admin/catalogs` (global reference data: crops, equipment types, units) + - `/admin/broadcast` (notification to all/selected tenants) + +- [ ] **PR #615 — Warehouse: grain receipt + inventory** *(TZ 9, TZ 10)* + - `/grain-storages` "Прийняти зерно" button: full form, creates GrainReceipt + GrainBatch + GrainBatchPlacement + StockLedgerEntry, recalculates StockBalance + - Verify `TransferGrainHandler` updates `GrainBatchPlacement` (known bug) + - `/inventory` full cycle: Draft → InProgress → Completed/Cancelled + - Inline editing of Counted column, auto-calculated Difference with color coding + - Progress indicator: "Підраховано X з Y (Z%)" + - Completion creates `InventoryAdjustment` ledger entries per diff, invalidates `StockBalance` cache + - Session history with read-only view after completion + +- [ ] **PR #616 — Notifications center** *(TZ 12)* + - Dropdown layout fix (min-width 400px, title inline) + - dayjs relativeTime with `uk` locale, proper thresholds (`X хв тому`, `X дн. тому`, `12 берез.`, etc.) + - Mark-all-read, clear-read working + - Deep-link click → navigates to target + marks read + - `/notifications` full page with filters (type, status, period), infinite scroll + - SignalR `NotificationHub` for real-time push + - Backend fix: `NotificationService` uses empty Identity Roles tables — migrate to enum role on `AppUser` + - Triggers verified: overdue op, tech repair, low fuel, low/over storage, sale completed, job failure + +- [ ] **PR #617 — Demo seeder + Mobile + PWA** *(TZ 11, TZ 13)* + - `Tools/DemoSeeder` idempotent: fills all core modules with connected Ukrainian-realistic data (6–12 months history) + - Auto-enables all optional feature flags for demo tenant after seed + - Mobile audit via Playwright at 390×844 and 360×640 across all routes + - Drawer sidebar, bottom nav (Головна / Поля / Склад / Техніка / Ще) + - Tables → Cards on fields, tech, operations, grain-storages, grain-batches, expenses, sales, personnel, rent-payments + - Forms: proper input types, sticky submit, 48px buttons + - Modals → bottom sheets on mobile + - PWA: manifest, service worker (Workbox), stale-while-revalidate assets, offline read-only for cached API data + +--- + +## Decisions locked (do not re-discuss, do not override without explicit approval in chat) + +**Currency** +- NBU JSON API: `https://bank.gov.ua/NBU_Exchange/exchange_site?start=YYYYMMDD&end=YYYYMMDD&valcode=USD&json` (and EUR) +- Base currency in DB is always UAH +- Conversion happens at presentation layer only +- `ExchangeRate (Code, Date, RateToUah)` with PK (Code, Date) +- Do NOT add `rate_at_transaction` to operation tables in this phase +- Weekend/holiday: fallback to previous business day's rate +- NBU unavailable: use last stored rate + log Warning + +**2FA** +- TOTP only (Otp.NET backend, otplib+qrcode frontend) +- No SMS, no email codes +- Mandatory for `IsSuperAdmin=true` accounts +- Optional for regular users, toggle in Налаштування → Безпека +- 10 single-use BCrypt-hashed backup codes +- Recovery on exhausted backup codes: manual reset by another super-admin with audit entry + +**Impersonation** +- Full login-as, NOT view-only +- Red persistent banner across entire UI, not dismissable, z-index 9999 +- 60min TTL, not renewable — must start new session +- Mandatory `reason` field (min 10 chars, free text) +- Email notification sent to target user on start +- Audit log: every mutation tagged with `impersonated_by` + `acted_as` +- Forbidden actions in impersonation: password change, email change, account deletion, API keys write scope, billing, full data export +- Rate limit: 3 sessions per (admin, target_user) per 24h +- ToS clause required for tenants + +**Feature flags** +- Per-tenant only, no per-user overrides +- Hardcoded enum of flag keys in code (not user-defined) +- `TenantFeatureFlags (TenantId, FeatureKey, IsEnabled, UpdatedAt, UpdatedBy)` +- IMemoryCache TTL 60s, invalidate on write +- Disabled flag on sidebar → menu item NOT rendered (not grayed out) +- Disabled flag on route → 404 (not 403, don't reveal feature existence) + +**Optional flag keys (these are the ones gated per tenant):** +``` +budget +pnl_by_fields +analytics.marginality +analytics.season_comparison +analytics.break_even +analytics.field_efficiency +analytics.resource_usage +analytics.expense_analytics +analytics.sales_analytics +``` + +**Core modules — always on, never gated:** +- Головна, Виробництво, Склад і логістика, Техніка, Персонал +- Фінанси: Витрати, Продажі, Орендні платежі, Зарплата та паливо +- Налаштування + +**Theme** +- Dark mode only, light mode removed in PR #604 + +**Season** +- Real `Seasons` table with `StartDate`/`EndDate`, NOT hardcoded year-list (PR #612) +- Each tenant can have custom seasonal boundaries (crops differ) + +--- + +## Deferred / cut from roadmap + +- **Billing module** — no monetization yet, no payment integration planned. Feature flags already demonstrate tenant-level plan differentiation. Revisit post-roadmap. +- **Per-user feature flags / beta testers list** — not this phase. Current stage is product stability, not A/B infrastructure. +- **`rate_at_transaction` on operations** — only Sales/PurchaseContracts will need frozen rates eventually, not all operations. Defer until concrete business case. + +--- + +## Technical debt (to schedule) + +- **Feature flags legacy fallback**: current behavior is "no records = all enabled" (protects existing tenants from PR #609 migration). Before prod has paying clients: run backfill to write explicit `IsEnabled=true` for all tenants, then switch fallback to "no records = all disabled". +- **MailKit 4.15.1** — moderate severity CVE (`GHSA-9j88-vvj5-vhgr`). Upgrade to latest in a chore PR. +- **`.gitignore` has `.mcp.json` listed 3 times** — cosmetic, clean up when touching the file next. +- **P.6 totals audit incomplete** — PR #607 migrated `/expenses` ВСЬОГО and `/sales` totalRevenue. Other pages not verified: `/rent-payments`, `/salary-fuel`, `/pnl-by-fields`. Schedule as chore PR or fold into PR #617 demo prep. + +--- + +## Agent protocol + +When starting a new PR: + +1. Read this file and `docs/TZ.md`. +2. Verify `git log --oneline -20` on main matches the "Completed" section here. +3. Start the PR listed under "In progress". Do not skip ahead. Do not redo closed work. +4. If roadmap and codebase disagree, STOP and ask in chat — do not guess. +5. Decisions in "Decisions locked" are final; do not re-discuss. +6. Squash-merge to main with a clean commit message. +7. After merge: update this file — move completed item to "Completed", promote next item from "Upcoming" into "In progress". Update status markers in `docs/TZ.md` for affected points. +8. Commit the roadmap update in the same PR or as first chore commit of the next PR. diff --git a/docs/TZ.md b/docs/TZ.md new file mode 100644 index 00000000..ecc8e307 --- /dev/null +++ b/docs/TZ.md @@ -0,0 +1,246 @@ +# AgroPlatform TZ — Original audit findings with fix specifications + +> Reference document. Captures the original scope of the audit-driven fix cycle. +> This file does NOT define PR order — see `docs/ROADMAP.md` for that. +> Use this file to understand business context and acceptance criteria for each point. +> +> Status markers: +> - `[CLOSED in PR #X]` — fully shipped, no remaining work +> - `[PARTIALLY CLOSED in PR #X; rest in PR #Y]` — partial scope shipped, rest scheduled +> - `[IN PROGRESS in PR #X]` — active work +> - `[PLANNED for PR #X]` — scheduled, not started +> - (no marker) — not yet scheduled + +--- + +## Project context + +- Stack: .NET 8 + React 18 + TypeScript + PostgreSQL/PostGIS + Docker +- Architecture: Clean Architecture / CQRS / MediatR, multi-tenancy via `X-Tenant-Id` + EF Core query filters +- Production: `agrotech-usa.com`, DigitalOcean droplet `64.226.83.68` +- Demo mode currently open for all — banner "Демо-режим" must remain +- UI language: Ukrainian + +--- + +## ПУНКТ 1 — Корневий маршрут веде на лендинг, а не на login `[CLOSED in PR #604]` + +**Problem:** Opening `https://agrotech-usa.com/` redirected to `/login`. Should be: unauthenticated → AgroHero landing; authenticated → `/dashboard`. + +**Shipped:** Root route renders landing for unauthenticated visitors (including public demo mode). `usePublicDemoAutoLogin` skips auto-login on `/`. In-app CTAs trigger login as before. + +--- + +## ПУНКТ 2 — Перемикач періоду на дашборді показує конкретні дати `[PARTIALLY CLOSED in PR #605, #607; full Season model in PR #612]` + +**Problem:** Dashboard buttons `День / Тиждень / Місяць / Сезон` didn't show which period was active. + +**Shipped in #605:** `?period=` URL query param on `/dashboard`; monospace label under segmented control shows resolved date range. +**Shipped in #607:** `‹ ›` anchor-date stepping with keyboard shortcuts (`←`/`→`), boundary-aware disable via `/api/tenant/data-boundaries`. +**Remaining for #612:** Full Season model with real StartDate/EndDate per tenant (currently uses hardcoded year-list for season boundaries — silent bug for non-standard crop cycles). + +--- + +## ПУНКТ 3 — Клік по графіку "Фінансовий огляд" → drill-down `[CLOSED in PR #605, #606]` + +**Problem:** Financial chart points were not clickable; could not see what composed a spike. + +**Shipped in #605:** Click on chart point navigates to `/economics/costs?from=…&to=…` with RangePicker pre-filled. +**Shipped in #606:** Same pattern for monthly revenue bar chart on `/sales/analytics` → navigates to `/sales`. + +--- + +## ПУНКТ 4 — Прибрати "Бюджет" з sidebar `[CLOSED in PR #609]` + +**Problem:** Budget module has no real business logic; market too volatile. + +**Shipped:** Budget hidden via feature flag (default off for new tenants). Code preserved under feature-flag gate. Direct route access returns 404 when flag disabled. + +--- + +## ПУНКТ 5 — Feature flags per tenant + super-admin UI for management `[CLOSED in PR #609 backend; super-admin UI in PR #610]` + +**Problem:** Not every company needs every module. Need per-tenant module toggles. + +**Always on (core):** Головна, Виробництво, Склад і логістика, Техніка, Персонал, Фінанси (Витрати, Продажі, Орендні платежі, Зарплата та паливо), Налаштування. + +**Optional (per-tenant toggle):** +- `budget` +- `pnl_by_fields` +- `analytics.marginality`, `analytics.season_comparison`, `analytics.break_even`, `analytics.field_efficiency`, `analytics.resource_usage`, `analytics.expense_analytics`, `analytics.sales_analytics` + +**Shipped in #609:** `TenantFeatureFlags` table, `IFeatureFlagService` with `IMemoryCache` TTL 60s, `[RequireFeatureFlag]` middleware (returns 404), `FeatureFlagGate` frontend component, `/api/me` payload includes features map. +**Shipped in #610:** Super-admin UI for per-tenant toggles at `/admin/tenants/:id`. +**Technical debt:** Legacy fallback "no records = all enabled" protects existing tenants. Before production has paying clients, backfill explicit flags and switch fallback to "all disabled". + +--- + +## ПУНКТ 6 — Карточка "ВСЬОГО" на /expenses + аудит всіх totals-карток `[PARTIALLY CLOSED in PR #607; remaining pages pending]` + +**Problem:** `/expenses` ВСЬОГО card was empty; similar issue on other pages. + +**Shipped in #607:** New `` shared component with highlight variant. `MaterialKpiCards` refactored to use it. `SalesList.totalRevenue` migrated. +**Remaining:** Verify and migrate totals on `/rent-payments`, `/salary-fuel`, `/pnl-by-fields`. Playwright sweep across all pages with totals cards. May fold into PR #617 demo prep or separate chore PR. + +--- + +## ПУНКТ 7 — Прибрати блок "Витрати план/факт за категоріями" з /expenses `[CLOSED in PR #603]` + +**Shipped:** Plan/Fact card removed from `/expenses`. Code preserved but not rendered. + +--- + +## ПУНКТ 8 — Прибрати light mode, додати selectable currency `[8.1 CLOSED in PR #604; 8.2 PLANNED for PR #613]` + +### 8.1 Light mode `[CLOSED in PR #604]` +**Shipped:** Sun/Moon toggle removed from topbar; `themeStore` locked to `'dark'`; `ConfigProvider` always uses dark theme. + +### 8.2 Currency selector `[PLANNED for PR #613]` +**Scope:** +- `UserPreferences.PreferredCurrency` (UAH/USD/EUR, default UAH) +- `ExchangeRate` table with PK (Code, Date) +- `NbuCurrencyService` + cron 06:00 Kyiv +- Backfill script `Tools/NbuBackfill` from 2024-01-01 +- `useFormatCurrency()` hook, settings UI in Профіль → Валюта +- Fallback: last stored rate on NBU failure; weekend/holiday → previous business day +- Exports: currency header with NBU rate on export date +- Base currency in DB always UAH; conversion at presentation layer only +- Do NOT add `rate_at_transaction` to operation tables in this phase + +--- + +## ПУНКТ 9 — Кнопка "Прийняти зерно" на /grain-storages `[PLANNED for PR #615]` + +**Problem:** "Прийняти зерно" button does nothing on click. + +**Scope:** +- Open modal with full grain receipt form: date, crop, warehouse, batch number (auto-generated), quantity, moisture/trash %, source (field/counterparty), driver/TTN/vehicle +- Validation: quantity > 0, warehouse capacity check, field must be in current season if source = field +- Creates `GrainReceipt` + `GrainBatch` + `GrainBatchPlacement` + `StockLedgerEntry` of type `GrainReceipt` +- Recalculates `StockBalance` +- Known bug from prior audit: verify `TransferGrainHandler` correctly updates `GrainBatchPlacement` + +--- + +## ПУНКТ 10 — Фікс модуля /inventory (Інвентаризація) `[PLANNED for PR #615]` + +**Problem:** Inventory module doesn't work end-to-end. Counted/Difference columns empty, progress shows garbage, session can't be completed. + +**Scope — full lifecycle:** +- Session states: `Draft → InProgress → Completed/Cancelled` +- Inline editing of Counted column, auto-calculated Difference with color coding (red/amber/green) +- Comment dropdown: Недостача / Списання / Помилка обліку / Пересорт / Інше +- Progress indicator: `Підраховано X з Y (Z%)` + bar +- Completion creates `StockLedgerEntry` of type `InventoryAdjustment` per non-zero diff +- `StockBalance` cache invalidated after completion +- Cancel flow without ledger changes +- Read-only view after completion +- History list with filters + +--- + +## ПУНКТ 11 — Демо-seed: заповнити дані по всіх модулях `[PLANNED for PR #617]` + +**Problem:** Demo tenant has empty modules (e.g., `/salary-fuel`). Cannot show full functionality to investors. + +**Scope:** Idempotent `Tools/DemoSeeder` fills all core + enabled optional modules with connected Ukrainian-realistic data over 6–12 months. Auto-enables all optional feature flags for demo tenant after seed. + +**Data coverage requirements:** +- Organization: ТОВ "Демо-Агро" in Poltava region +- Fields: 83 fields (already seeded), add crop rotation for 2024/25/26 +- Seasons: 2024 (closed), 2025 (closed), 2026 (active) +- Warehouses: grain storages with batches of wheat/corn/sunflower/rapeseed/soy; inventory items (КАС-32, NPK, seeds, chemicals, fuel, parts) +- Equipment: 15–20 units (John Deere, Case IH, МТЗ, Claas, КамАЗ) +- Personnel: 30–50 employees with real positions and Ukrainian names +- Operations: 300–500 ops linking fields/equipment/operators/stock +- Expenses: all 6 categories populated +- Sales: 8–12 contracts with real traders (Kernel, Nibulon, Bunge) +- Rent payments: 150–300 landholders with contracts and partial payouts +- Salary & fuel: 3–6 months of timesheets and fuel distributions +- Notifications: 10–15 realistic triggers +- API keys: 1–2 demo keys with different scopes + +**Idempotency:** Use `tenant.Code = 'DEMO'` and `entity.SeedKey` to prevent duplication on rerun. + +--- + +## ПУНКТ 12 — Центр сповіщень: фікс UI і логіки `[PLANNED for PR #616]` + +**Problem:** Dropdown title renders vertically per letter, time shows "2227h ago" in English, actions may not work, read/unread visually indistinguishable. + +**Scope:** +- Dropdown min-width 400px (mobile: full-width bottom sheet) +- Title "Сповіщення · X нових" in one line +- Color border by severity (info=blue, warning=amber, critical=red, success=green) +- Unread indicator dot, read items dimmed 60% +- dayjs `relativeTime` with `uk` locale: `щойно`, `X хв тому`, `X год тому`, `вчора о HH:mm`, `X дн. тому`, `12 берез.`, `DD MMM YYYY` +- Actions: `Прочитати все` (PATCH mark-all-read), `Очистити прочитані` (DELETE read only) +- Click → navigate to `targetUrl` + mark as read +- Hover → individual "×" for single delete +- Dropdown shows first 10, footer link `Усі сповіщення →` leads to `/notifications` +- `/notifications` page: filters (type/status/period), infinite scroll or pagination by 50 +- SignalR `NotificationHub` for real-time push, increments badge counter +- **Backend fix:** `NotificationService` currently uses empty Identity Roles tables. Migrate to enum role on `AppUser`; targeting via `NotificationRecipient` table or `target_role` enum with user selection from tenant. +- Triggers to verify working: overdue operation, tech in repair, low fuel, low/over storage, sale completed, fuel issue, background job failure + +--- + +## ПУНКТ 13 — Повний mobile audit і переробка `[PLANNED for PR #617]` + +**Problem:** Mobile version not designed for field use. + +**Scope:** +- Playwright audit at 390×844 (iPhone 14) and 360×640 (Android baseline) across all routes +- Screenshots saved to `/audit/mobile/{route-slug}/` +- **AppShell:** sidebar → drawer with overlay, toggled by hamburger, closable by swipe-left or overlay tap. Top bar sticky 56px. Bottom navigation bar (breakpoint <768px) with 5 items: Головна / Поля / Склад / Техніка / Ще +- **Tables → Cards** on mobile: `/fields`, `/tech`, `/operations`, `/grain-storages`, `/grain-batches`, `/expenses`, `/sales`, `/personnel`, `/rent-payments`. Action menu as kebab → bottom sheet. +- **Forms:** full width, `type="number"` with `inputmode="decimal"`, `type="tel"`, `type="date"`, 16px inputs (no iOS zoom), sticky 48px submit button +- **Dashboard:** KPI cards single-column, charts with tap tooltips, Leaflet with larger zoom controls +- **Modals → bottom sheets** on mobile with drag handle +- **Search/filters:** dedicated "Фільтри" button → full-screen panel +- **PWA:** `manifest.webmanifest` with icons (192, 512), theme color `#0a0a0a`. Service worker (Workbox): stale-while-revalidate for assets, cache read-only API (`/api/fields`, `/api/operations?status=active`, `/api/dashboard`) with TTL 5min. Offline banner when no connection. +- **Acceptance:** Lighthouse mobile ≥ 90 (Performance/Accessibility/Best Practices). Install to home screen works on iOS Safari + Android Chrome. + +--- + +## ПУНКТ 14 — Супер-адмін має бути реально супер-адміном `[FOUNDATION CLOSED in PR #610; advanced features in PR #614]` + +**Problem:** Previous "super-admin" role was tenant-scoped, couldn't see other tenants. + +### Foundation `[CLOSED in PR #610]` +**Shipped:** +- `IsSuperAdmin` boolean on `AppUser` (global, outside tenant roles) +- JWT claim `is_super_admin`, separate claim `mfa_verified` +- `[SuperAdminRequired]` middleware returning 403 + `X-Mfa-Required` header when MFA not set up +- `AdminController` with 5 endpoints, all using `IgnoreQueryFilters()` for cross-tenant access +- TOTP MFA mandatory for super-admin (Otp.NET, 10 BCrypt-hashed backup codes, ±1 step tolerance) +- `SuperAdminAuditLog` table + `SuperAdminAuditService`, entries on every mutation +- Pages: `/admin/tenants`, `/admin/tenants/:id` with Features tab +- 8 integration tests (PR #611) + +### Advanced features `[PLANNED for PR #614]` +**Scope:** +- **Impersonation:** + - `POST /api/admin/impersonate` with mandatory `reason` field + - 60min TTL, not renewable + - Red persistent banner across UI during impersonation + - Email notification to target user on start + - Rate limit: 3/day per (admin, target_user) pair + - Forbidden actions: password/email change, API keys with write scope, billing, full data export + - Audit: every mutation tagged `impersonated_by` + `acted_as` +- **`/admin/users`** — global search across tenants, impersonate button +- **`/admin/audit-log`** — global view with filters (tenant, user, action type, period), CSV export +- **`/admin/system`** — queue/jobs health, storage usage, active SignalR connections, error rate +- **`/admin/catalogs`** — manage global reference data (crops, equipment types, units, document types) +- **`/admin/broadcast`** — send notifications to all or selected tenants, with history + +--- + +## Notes on execution rules + +- Cannot skip ahead in the roadmap order +- Cannot redo closed work +- Decisions in `docs/ROADMAP.md` "Decisions locked" section are final +- Ask in chat before re-interpreting scope +- Each PR: typecheck + tests green before commit; squash-merge to main +- Update `ROADMAP.md` and this file after every merge diff --git a/docs/plan.md b/docs/plan.md index fb80780e..73a0159e 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -5,8 +5,57 @@ Living document. Each PR collapses a slice of ТЗ into a shippable unit. ## Completed - PR #609 (merged as d30972e): tenant optional feature flags + budget gating (ТЗ #13). +- PR #610 (merged as d208689): super-admin foundation — TOTP MFA, audit log, tenant admin UI (ТЗ #14, first half). +- PR #611 (merged as d358a24): integration tests for super-admin auth + MFA + audit. -## In flight — PR #610: super-admin foundation (ТЗ #14, first half) +## In flight — PR #612: Season model (ТЗ #2, completes dashboard seasonal scope) + +Branch: `pr612-season-model`. + +### Why now + +Dashboard seasonal UI (PR #605/#607) currently uses a year-list derived from operation/cost/sale timestamps. This is semantically wrong for farms with non-calendar crop cycles (Ukraine convention: Aug 1 → Jul 31). Users see "Season 2025" rather than "Сезон 2025/2026: 1 серп. 2025 — 31 лип. 2026", and `‹ ›` navigation steps by integer year, not by real season. This PR promotes Season to a first-class, tenant-configurable entity. + +### Backend + +1. Domain entity `Season : AuditableEntity` with Code(16), Name(100), StartDate, EndDate, IsCurrent. +2. Migration `AddSeasons`: + - Table + unique `(TenantId, Code)` + CHECK `EndDate > StartDate` + partial unique for `IsCurrent = true` per tenant. + - Idempotent data seed: for each existing tenant, create `Сезон 2023/2024`, `Сезон 2024/2025`, `Сезон 2025/2026` (current) if no seasons exist. +3. `/api/seasons` response shape changes from `int[]` → `SeasonDto[]` (breaking; one frontend consumer). +4. Tenant-scoped CRUD at `/api/seasons` (GET list, GET current, POST, PUT, DELETE, POST set-current). Admin-only for mutations. +5. Super-admin CRUD at `/api/admin/tenants/{id}/seasons` — `[SuperAdminRequired]`, audited via `ISuperAdminAuditService`. +6. `SetCurrent` is transactional (flips IsCurrent for the tenant). +7. `Delete` safety net: rejects when cost / sale / operation rows fall inside the season's date window. + +### Frontend + +1. Types + `useTenantSeasons` hook return `SeasonDto[]`. +2. Dashboard arrow navigation iterates through `SeasonDto[]` by StartDate, labels use real dates (`"Сезон 2025/2026: 1 серп. 2025 — 31 лип. 2026"`). +3. New `/settings/seasons` for tenant admin (table + modals). +4. New `/admin/tenants/:id/seasons` mirror for super-admin. +5. i18n keys added to `uk.ts` + `en.ts`. + +### Tests (mandatory) + +1. Migration idempotency (run twice, no duplicates). +2. SetCurrent transactional (exactly one IsCurrent after flip). +3. Delete safety — blocked with linked data. +4. `GET /api/seasons` as tenant-admin returns only own tenant's rows. +5. Super-admin `POST /set-current` → 200 + exactly one new `SuperAdminAuditLog`. +6. Same endpoint without super-admin claim → 403. + +### Out of scope + +- Tying AgroOperation/CostRecord/Sale rows to a `SeasonId` FK (separate PR — backfill strategy). +- Per-crop cycle overrides. +- E2E dashboard scroll test (deferred — no Playwright pipeline in CI today). + +### Deployment note + +Data migration auto-seeds 3 default seasons (Aug–Jul boundaries) for existing tenants. Review and adjust per tenant before PR #613 (planned: currency + historical FX), which will rely on accurate season boundaries for annual reports. + +## Archived — PR #610 scope (super-admin foundation, first half) Branch: `pr610-super-admin-foundation`. Single PR as agreed (no split). @@ -87,6 +136,7 @@ Coexistence with the existing role-based super-admin: ## Upcoming -- PR #611 — multi-tenant DNS / org-level routing. -- PR #612 — billing + plan enforcement. -- PR #613 — super-admin phase 2 (users, audit UI, billing, impersonation). +- PR #613 — currency + historical FX (depends on Season boundaries from #612). +- PR #614 — super-admin phase 2 (users, audit UI, impersonation). +- PR #615 — tie `SeasonId` FK onto AgroOperation / CostRecord / Sale (depends on #612). Backfill by date-range. +- Post-roadmap (no firm need yet) — multi-tenant DNS / org-level routing; billing + plan enforcement. Feature flags already demonstrate plan differentiation; billing deferred until monetization is a concrete goal.