Перейти до змісту

Архітектурний аудит проєкту ESWF

Дата: 2026-03-26 Роль: Lead System Architect + Risk Management


Масштаб проєкту

Метрика Значення
Сирцевий код ~146,000 рядків у ~995 файлах
Фронтенди 4 (Portal, News, DOP, Shop)
Мобільні додатки 2 (Driver, Sales)
Бекенд Django apps 15 модулів, 95+ моделей
Міграції БД 153+
Тести 0

1. КРИТИЧНІ РИЗИКИ (зупинять проєкт)

1.1. Абсолютний нуль тестового покриття

  • 0 тестів на бекенді (14 apps, усі tests.py — порожні заглушки)
  • 0 тестів на фронтенді (65,000 рядків DOP без жодного .test.tsx)
  • 0 тестів у мобільних додатках
  • Навіть тестові бібліотеки (Vitest, Jest, pytest) не встановлені

Наслідки: - Будь-який рефакторинг — рулетка - Неможливо безпечно оновити залежності - Неможливо онбордити нового розробника - Регресії накопичуються непомітно

1.2. Відсутність Error Boundaries у фронтенді

Жодного ErrorBoundary компоненту в усьому DOP. Якщо будь-який компонент кине помилку під час рендеру — весь додаток падає на білий екран.

1.3. Монолітні "god-компоненти"

Файл Рядків Проблема
AdminToolsPage.tsx 2,010 UI + логіка + API + стейт в одному файлі
ChatPage.tsx 1,464 WebSocket + рендеринг + бізнес-логіка
ChatDrawer.tsx 1,345 Той самий антипатерн
SalesFieldManager.tsx 1,265 Моноліт
ETTNPage.tsx 1,077 Ed25519 + UI + форми

2. ВИСОКІ РИЗИКИ (нагромаджуються)

2.1. Нуль моніторингу та observability

  • Немає Sentry / Datadog / New Relic
  • Немає структурованого логування
  • Немає health-check ендпоінтів
  • Немає метрик (Prometheus, StatsD)
  • Немає APM

2.2. Відсутність CI/CD пайплайну

  • Немає GitHub Actions / GitLab CI
  • Немає Dockerfile / docker-compose
  • Немає автоматизованих перевірок при PR

2.3. Якість git-історії

Останні 20 комітів — переважно "fix" без контексту. Неможливо зрозуміти що було змінено і навіщо.

2.4. Безпека UniversalViewSet

  • Sort field інтерполюється напряму в order_by() без whitelist
  • Filter parameters приймають довільні імена полів
  • Немає ліміту pageSize — запит ?pageSize=999999 може вбити сервер
  • Google OAuth створює юзерів для будь-якого Google-акаунту без whitelist доменів

2.5. Немає code splitting у фронтенді

Усі роути імпортуються на top-level. Немає React.lazy(). Весь DOP завантажується одним бандлом.


3. СЕРЕДНІ РИЗИКИ (технічний борг)

3.1. 153+ міграцій без squash

essentials і fleet мають по 43-44 міграції кожний.

3.2. Accessibility (a11y) — практично відсутня

З 188 TSX-файлів лише 2 мають aria-label. Не пройде WCAG 2.1 AA.

3.3. API-клієнт без timeout і retry

JWT refresh реалізований коректно, але немає request timeout і retry стратегії.

3.4. Одна версія API без deprecation strategy

Усе на /api/v1/ — немає плану версіонування, немає CHANGELOG.

3.5. Зберігання API-ключів ContainerHub

api_key зберігається як звичайний CharField. Повинні шифруватися at rest.

3.6. Монорепо без інструментів монорепо

6 фронтендів + 2 мобільних + бекенд без Nx / Turborepo. Порожня frontend/shared/.


4. ЩО ДОБРЕ

Аспект Оцінка
TypeScript strict mode Увімкнений, агресивний лінтинг
Секрети .env в .gitignore, env vars у settings
WebSocket auth JWT + tenant isolation + membership check
Multi-tenancy Коректна ізоляція через middleware + abstract models
PWA Workbox + offline API caching + auto-update
Zustand stores Чисто розділені, правильна персистенція
i18n en/ua across усіх фронтендів
Django production settings SSL, HSTS, secure cookies
Metadata-driven UI EntityRegistry + UniversalViewSet

5. ПЛАН УСУНЕННЯ

Фаза 1 — Стабілізація (тижні 1-4)

  • [x] Vitest + React Testing Library для DOP — 80 тестів (utils, stores, API, LoginPage, ErrorBoundary)
  • [ ] pytest для бекенду — покрити UniversalViewSet, permissions, serializers
  • [x] Error Boundaries — глобальний (App.tsx) + per-section (SectionPage.tsx) + 8 тестів
  • [x] Sentry — інтеграція (frontend + backend)
  • [x] Ліміт pageSize (1-500) і whitelist sort/filter полів у UniversalViewSet

Фаза 2 — Інфраструктура (тижні 5-8)

  • [ ] GitHub Actions — lint + typecheck + test на кожен PR
  • [ ] .env.example + setup скрипт — документований onboarding
  • [x] Code splitting — React.lazy() для маршрутів, vendor chunks (index.js: 6MB→1.6MB, -74%)
  • [ ] Request timeout + retry в API клієнті
  • [ ] Conventional commits — husky + commitlint

Фаза 3 — Масштабування (тижні 9-12)

  • [ ] Рефакторинг god-компонентів (1000+ рядків)
  • [ ] Squash міграцій
  • [ ] API versioning strategy
  • [ ] Accessibility audit
  • [ ] Шифрування API-ключів at rest

6. SENTRY — ІНСТРУКЦІЯ З ПІДКЛЮЧЕННЯ

Як отримати DSN

  1. Зареєструватися на sentry.io (безкоштовний план: 5K помилок/міс)
  2. Створити організацію → створити 2 проєкти:
  3. ESWF-Frontend (платформа: React)
  4. ESWF-Backend (платформа: Django)
  5. У кожному проєкті: Settings → Client Keys (DSN) → скопіювати DSN

DSN має формат: https://<public_key>@o<org_id>.ingest.sentry.io/<project_id>

Змінні оточення

# Frontend (.env або .env.production)
VITE_SENTRY_DSN=https://xxx@o123.ingest.sentry.io/456

# Backend (.env)
SENTRY_DSN=https://yyy@o123.ingest.sentry.io/789
SENTRY_ENVIRONMENT=production   # або staging, development

Що вже інтегровано

Frontend (DOP): - src/lib/sentry.ts — initSentry(), captureError(), setSentryUser(), clearSentryUser() - main.tsx — initSentry() викликається перед рендером - App.tsx — глобальний ErrorBoundary передає помилки в Sentry - SectionPage.tsx — per-section ErrorBoundary передає помилки в Sentry - authStore.ts — login/logout встановлює/очищує користувача в Sentry - Автоматично: BrowserTracing (performance), Session Replay (10% помилок) - У dev-режимі помилки не відправляються (beforeSend фільтрує localhost)

Backend (Django): - settings/base.py — sentry_sdk.init() при наявності SENTRY_DSN - traces_sample_rate=0.2 (20% запитів для performance) - profiles_sample_rate=0.1 (10% профілювання) - send_default_pii=True (прив'язка помилок до користувачів)

Без DSN

Якщо SENTRY_DSN / VITE_SENTRY_DSN не задані — Sentry не ініціалізується, overhead нульовий. Додаток працює як раніше.


7. ІНСТРУКЦІЯ — ЯК ЗАПУСКАТИ ТЕСТИ І ПЕРЕВІРЯТИ КОРЕКТНІСТЬ

Швидкий старт

cd c:\eswf\frontend\erp

Запуск тестів

Команда Що робить
npx vitest run Запустити всі тести один раз і вийти
npx vitest Watch-режим — тести перезапускаються при зміні файлів
npx vitest run src/store/ Запустити тести лише з папки src/store/
npx vitest run src/utils/vatUtils.test.ts Запустити конкретний файл тестів
npx vitest run --reporter=verbose Детальний вивід (кожен тест окремо)

Покриття коду (coverage)

npx vitest run --coverage

Згенерує звіт у терміналі: скільки % рядків, функцій та гілок покрито тестами.

Що зараз покрито тестами (80 тестів)

Файл Кількість Що тестує
src/utils/vatUtils.test.ts 17 ПДВ розрахунки (calcVatLine, calcDocumentTotals, effectiveRate)
src/api/entities.test.ts 7 toApiPath — конвертація camelCase → kebab-case
src/api/client.test.ts 4 JWT interceptor, refresh token, logout на 401
src/store/authStore.test.ts 7 Login, logout, tokens, bootstrapped state
src/store/tabsStore.test.ts 10 Відкриття/закриття вкладок, activeTab
src/store/uiStore.test.ts 12 Тема, мова, sidebar, preferences sync
src/store/settingsStore.test.ts 8 Бухгалтерські налаштування, чат
src/pages/LoginPage.test.tsx 6 Рендер форми, помилки, навігація
src/components/shared/ErrorBoundary.test.tsx 8 Перехоплення помилок, recovery, fallback

Перевірка збірки

# Перевірити що проєкт збирається без помилок
cd c:\eswf\frontend\erp
npx vite build

Якщо збірка успішна — побачиш список чанків з розмірами і ✓ built in X s.

Перевірка бекенду

cd c:\eswf\backend

# Перевірити що Django стартує без помилок
python manage.py check

# Перевірити міграції
python manage.py migrate --check

Коли запускати тести

  • Після зміни бізнес-логіки (vatUtils, stores, API client) → npx vitest run
  • Після оновлення залежностей (npm update) → npx vitest run + npx vite build
  • Перед комітом — переконатись що нічого не зламалось
  • При додаванні нового коду — написати тест поруч (файл *.test.ts або *.test.tsx)

Як додати новий тест

  1. Створи файл поруч з модулем: myModule.test.ts (або .test.tsx для компонентів)
  2. Імпортуй те, що тестуєш:
    import { myFunction } from './myModule';
    
  3. Напиши тест:
    describe('myFunction', () => {
      it('should return correct result', () => {
        expect(myFunction(2, 3)).toBe(5);
      });
    });
    
  4. Для React компонентів використовуй хелпер:
    import { renderWithProviders } from '@/test/renderWithProviders';
    import { screen } from '@testing-library/react';
    
    it('renders title', () => {
      renderWithProviders(<MyComponent />);
      expect(screen.getByText('Title')).toBeInTheDocument();
    });
    
  5. Запусти: npx vitest run src/path/myModule.test.ts

Структура тестової інфраструктури

src/test/
├── setup.ts                 # Глобальний setup (jest-dom, mocks для window.matchMedia, ResizeObserver)
└── renderWithProviders.tsx   # Обгортка: MantineProvider + MemoryRouter + QueryClient + i18n

Конфігурація: vitest.config.ts у корені frontend/erp/.


App taxonomy (2026-04-28)

DOP has 4 application categories. Source of truth — frontend config/types.ts SectionItem.appType + backend shop.Product.app_type.

Type Role Examples
core Base system, cannot be uninstalled Essentials
module Vertical functional block (own backend app, own section) Fleet, Logistic, ContainerHub
plugin Functional layer on top of a module — most horizontal apps live here Accounting, CRM, HRM & Payroll, Production, Budgeting, Consolidation, FleetTrack, E-Commerce Manager, Purchase Invoice, Quality Control, DOP Chat
integration Connector to an external system M.E.Doc, Bank Exchange, BAF Sync, eTTN

addon is deprecated — renamed to plugin 2026-04-28 (shop migration 0009, frontend applications.ts mass-edit). The old term still appears in legacy commit messages and product docs but should not be reintroduced.

Sidebar.tsx renders Mantine Accordion: - top level — core + module items where !sidebarHidden - inside each branch — two buckets, Plugins and Integrations, populated from forModules[0] (or sidebarParent override) - hidden from sidebar but kept in the AppStore catalogue: appstore, appPortal (separate Next.js site), appDriverApp (native mobile)

Open branches persist in uiStore.sidebarAccordionOpen (default ['appEssentials']).

Tree builder: buildSidebarTree — sibling of buildDependencyTree, both consume the same applications.ts items. Plan: docs/planning/accordion-sidebar.md.