System Architecture & Developer Guide

Версия: 3.0

1. Обзор Системы и Стек Технологий

Настоящая документация описывает архитектуру промышленного уровня для системы онлайн-тестирования, обучения и поведенческого прокторинга. Документ призван служить единым, авторитетным источником информации для всех специалистов, взаимодействующих с системой, от новых инженеров-разработчиков до инженеров по эксплуатации (DevOps), отвечающих за ее круглосуточную доступность и надежность.

1.1. Архитектура Высокого Уровня

Система спроектирована на базе Flask и реализует архитектурный паттерн "Фабрика приложений" (реализованный через функцию create_app в __init__.py). Следует особо подчеркнуть, что данный паттерн был выбран ввиду его фундаментальной гибкости. Он позволяет осуществлять отложенную инициализацию (lazy initialization) приложения и его расширений, что дает возможность динамически создавать экземпляры приложения с различными конфигурациями (например, для production, development или testing) путем внедрения различных объектов конфигурации. Этот подход также эффективно решает проблему циклических импортов (circular imports) на уровне Python, что является частой проблемой в крупных Flask-приложениях.

Архитектура демонстрирует строгое разделение обязанностей (Separation of Concerns) между компонентами, что способствует высокой модульности и упрощает сопровождение:

Система в полной мере использует модель взаимодействия Backend for Frontend (BFF):

  1. Клиент (браузер) первоначально получает базовую HTML-оболочку от web/routes.py, которая служит лишь точкой входа.
  2. Внутри этой оболочки загружается и выполняется клиентский JavaScript (например, приложение 117-test.html или админ-панель results.html), который берет на себя всю ответственность за пользовательскую логику и рендеринг интерфейса.
  3. Данный JS-код активно взаимодействует с защищенным JSON API (api/routes.py), отправляя данные (например, logEventToServer) и получая их для отображения.
  4. Параллельно, SocketIO обеспечивает постоянный двунаправленный канал связи. Бэкенд (после успешной транзакции save_results) использует Redis Pub/Sub для широковещательной рассылки события update_needed всем подключенным админ-панелям. Это позволяет им инициировать дельта-обновление (delta update) своего интерфейса без необходимости полной перезагрузки страницы, обеспечивая высокий уровень оперативности.

1.2. Технологический Стек

Детальный перечень компонентов стека и их роли в архитектуре системы:

Компонент

Технология

Назначение и Ключевые Файлы

Фреймворк

Flask, Flask-SocketIO

Легковесное ядро приложения, реализующее роутинг и обработку HTTP-запросов. Flask-SocketIO обеспечивает интеграцию real-time WebSocket-коммуникаций поверх Flask, используя модель событий.

База данных

Flask-SQLAlchemy (PostgreSQL)

ORM для взаимодействия с реляционной базой данных PostgreSQL. Абстрагирует SQL-запросы, позволяет определять модели данных в Python. Следует отметить использование специфичных для PostgreSQL функций, таких как sqlalchemy.dialects.postgresql.insert (предположительно, для реализации ON CONFLICT DO UPDATE в get_or_create_user) и пессимистичных блокировок (with_for_update() в document.py). Последнее является критическим механизмом для обеспечения целостности данных (data integrity) в условиях высоких конкурентных нагрузок, в частности, для предотвращения "гонки состояний" (race conditions) при генерации уникальных номеров документов.

Кэширование и Очереди

Redis

Высокопроизводительное in-memory K-V хранилище, выполняющее в системе четыре критически важные и раздельные роли. Эта многофункциональность централизует управление эфемерным состоянием, оптимизирует производительность и снижает количество внешних зависимостей.

1. Кэш приложения (Flask-Cache): Кэширование результатов ресурсоемких запросов к API, например, агрегированной статистики (get_filtered_stats).

2. Бэкенд для SocketIO (Message Queue): Ключевой компонент, обеспечивающий горизонтальную масштабируемость. Использует Redis Pub/Sub для передачи real-time сообщений между несколькими экземплярами (воркерами) Flask, что является обязательным требованием для production-окружения, работающего за балансировщиком нагрузки.

3. Ограничитель скорости (Flask-Limiter): Защита API-эндпоинтов от DoS-атак и брутфорса путем отслеживания частоты запросов с IP-адресов.

4. Кэширование безопасности: Отдельное, высокоскоростное кэширование проверенных API-ключей (в decorators.py) для минимизации обращений к БД (read-heavy) при каждом API-вызове.

Аутентификация

Гибридная (Flask-Login + decorators.py)

Гибкая, двухуровневая модель доступа, обслуживающая разные классы клиентов (человек vs. машина):

1. Flask-Login: Управляет традиционными веб-сессиями на основе httpOnly cookies для пользователей-людей (администраторов), использующих SPA-дашборд (results.html).

2. Кастомные API-ключи: Используются для программного (M2M) доступа (фронтенд 117-test.html, CI/CD скрипты) через декоратор @api_key_required. Декоратор @admin_required элегантно объединяет обе проверки (session OR key), позволяя одному и тому же защищенному эндпоинту быть доступным как для человека-администратора, так и для автоматизированного скрипта.

Валидация

Pydantic и Flask-WTF

Два специализированных, непересекающихся инструмента для решения различных задач валидации на разных уровнях:

1. Pydantic (result_schema.py): Обеспечивает строгую валидацию структуры данных (входящих JSON payload) на границе API. Гарантирует, что поле raw_data в базе данных всегда соответствует согласованной схеме SaveResultsRequest, защищая от некорректных или вредоносных данных (data sanitization) на входе.

2. Flask-WTF (forms.py): Обеспечивает валидацию пользовательского ввода (HTML-форм, как LoginForm) и, что более важно, предоставляет интегрированную CSRF-защиту для веб-сессий администратора.

Безопасность

Flask-WTF (CSRF), Flask-CORS, Flask-Limiter, X-CSRFToken

Многоуровневая эшелонированная защита (defense-in-depth):

CORS: Контролирует, какие домены (origins) имеют право обращаться к API.

Limiter: Защита от брутфорса и DoS-атак на уровне IP.

CSRF (WTF): Защита веб-сессий админа от атак межсайтовой подделки запроса (реализована через полный цикл: Flask-WTF генерирует токен, web/routes.py внедряет его в meta-tag, admin/api.js считывает его и отправляет в заголовке X-CSRFToken при каждом запросе).

XSS (escapeHtml): Санитизация вывода в ui.js админ-панели для предотвращения атак межсайтового скриптинга.

Мониторинг

Prometheus (prometheus_flask_exporter)

Сбор и экспорт production-метрик (как стандартных http_requests_total, так и кастомных TESTS_COMPLETED_TOTAL, ANALYSIS_DURATION_SECONDS) для системы мониторинга Prometheus. Это обеспечивает наблюдаемость (observability) системы и возможность проактивного обнаружения проблем.

Логирование

Структурированное JSON-логирование (logging.py)

Система логирования, соответствующая промышленным стандартам. Каждый лог пишется в формате JSON и обогащается request_id. Это позволяет системам агрегации логов (ELK Stack, Splunk, Graylog) легко парсить, индексировать и, что наиболее важно, коррелировать все события, связанные с одним конкретным HTTP-запросом (end-to-end tracing) для быстрой диагностики инцидентов.

Аналитика

fastdtw (Гибридная)

Алгоритм Dynamic Time Warping. Используется для сравнения временных последовательностей разной длины (траекторий движения мыши). Присутствует и на бэкенде (analytics.py) для глубокого серверного анализа, и на клиенте (admin/analysis.js), предоставляя администратору гибкий выбор (trade-off) между скоростью (мгновенный анализ на клиенте) и исчерпывающей точностью (более медленный, но авторитетный анализ на сервере).

Миграции БД

Flask-Migrate

Инструмент (обертка над Alembic) для управления изменениями схемы базы данных. Позволяет применять и откатывать миграции (flask db upgrade) при развертывании, обеспечивая версионирование структуры БД.

Клиентский PDF

jsPDF, html2canvas

Используются в 117-test.html для гибридной генерации PDF. html2canvas создает растровое изображение HTML-шаблона, а jsPDF упаковывает его в PDF-документ на стороне клиента. Это значительно снижает нагрузку на app сервис, освобождая его CPU и память от ресурсоемких задач рендеринга. Эта архитектура переносит нагрузку по генерации PDF на машину конечного пользователя.

Контейнеризация (DevOps)

Docker & Docker Compose

Система упакована в Docker-контейнеры для обеспечения изоляции зависимостей и воспроизводимости окружения. docker-compose.yml (динамически формируемый deploy.sh) оркестрирует запуск и связывание всех сервисов (postgres, redis, app, nginx) в единую изолированную сеть.

Веб-сервер (DevOps)

Nginx

Функционирует в роли High-Performance Reverse-Proxy и "фасада" всей системы. Он принимает весь внешний трафик, терминирует SSL (обслуживает HTTPS), раздает статические ассеты (JS, CSS, изображения, собранные flask collect) напрямую из static_data volume (не нагружая Flask), перенаправляет запросы к /api и /socket.io на app сервис (Gunicorn/Flask) и выполняет первый эшелон rate-limiting.

SSL (DevOps)

Certbot

Интегрирован в deploy.sh и docker-compose.yml (при флаге --use-letsencrypt). Автоматически получает и, что важно, автоматически обновляет SSL-сертификаты от Let's Encrypt через ACME-challenge.

Оркестрация (DevOps)

Bash (deploy.sh)

Интеллектуальный скрипт-оркестратор для "нулевого развертывания" (zero-touch deployment). Он не просто запускает сервисы (docker compose up), а выполняет полный цикл инициализации всей системы с нуля, включая установку Docker, динамическую генерацию всех конфигурационных файлов (.env, docker-compose.yml, nginx.conf), получение SSL-сертификатов и первоначальное наполнение БД.

Управление (DevOps)

Makefile

Сгенерированный deploy.sh интерфейс управления (CLI Dashboard). Предоставляет простой, высокоуровневый и запоминаемый интерфейс (make backup, make logs) как слой абстракции над более сложными и длинными docker compose и flask командами, что кардинально упрощает ежедневное обслуживание системы.

1.3. Диаграмма Последовательности (End-to-End Flow)

Следующая диаграмма последовательности (Mermaid) представляет собой центральный визуальный артефакт данной документации. Она детализирует полный жизненный цикл одного пользовательского теста, демонстрируя хронологическое взаимодействие всех девяти ключевых компонентов системы: от первоначального GET-запроса пользователя до финального real-time уведомления администратора.

Эта диаграмма имеет критическое значение для понимания того, как потоки данных (data flows) проходят через систему, как обрабатываются атомарные транзакции и как компоненты (БД, Кэш, Pub/Sub) взаимодействуют для обеспечения как целостности данных (data integrity), так и оперативности их отображения. Необходимо обратить внимание на три ключевых аспекта:

  1. Асинхронная проверка API-ключа, использующая кэш Redis для оптимизации.
  2. Атомарная транзакция save_results, включающая пессимистическую блокировку DocumentCounter для предотвращения "гонки состояний".
  3. Широковещательный механизм уведомления через Redis Pub/Sub, который отделяет логику API от real-time оповещения.

sequenceDiagram
   actor User as Пользователь (Браузер)
   participant Web as Веб-сервер (web/routes.py)
   participant API as API-сервер (api/routes.py)
   participant Auth as Декораторы (@api_key_required)
   participant Cache as Redis (Кэш)
   participant DB as БД (SQLAlchemy)
   participant Socket as Сервер SocketIO
   participant PubSub as Redis (Pub/Sub)
   actor Admin as Администратор (Браузер)

   %% --- Фаза 0: Администратор подключается (Заранее) ---
   Admin->>+Socket: Подключение по WebSocket
   Socket-->>-Admin: Соединение установлено
   Note right of Admin: Админ ожидает событий

   %% --- Фаза 1: Пользователь начинает тест ---
   User->>+Web: GET /117test (или /index.html)
   Web-->>-User: HTML-страница (117-test.html)
   
   group Фаза 1: log_event('test_started')
       User->>+API: POST /api/log_event (eventType: 'test_started', X-API-Key: ...)
       
       %% --- Проверка API-ключа ---
       API->>+Auth: @api_key_required
       Auth->>+Cache: cache.get(api_key) ?
       Cache-->>-Auth: (miss)
       Auth->>+DB: ApiKey.query(key) ?
       DB-->>-Auth: key_obj
       Auth->>+Cache: cache.set(key_obj)
       Auth->>+DB: UPDATE ApiKey SET usage_count...
       Auth-->>-API: Доступ разрешен
       
       %% --- Логика 'log_event' ---
       API->>+DB: db.session.get(ResultMetadata, session_id) ?
       DB-->>-API: None (Сессия не найдена)
       API->>DB: INSERT ResultMetadata (session_id, test_type="pending")
       API->>DB: INSERT ProctoringEvent (eventType='test_started')
       API->>DB: db.session.commit()
       API-->>-User: 200 OK
   end

   loop Пользователь проходит тест
       User->>API: POST /api/log_event (eventType: 'focus_loss', ...)
       %% (Повторяется Фаза 1, но сессия уже найдена)
       API->>User: 200 OK
   end

   %% --- Фаза 2: Пользователь завершает тест ---
   group Фаза 2: save_results
       User->>+API: POST /api/save_results (JSON с результатами, X-API-Key: ...)
       
       %% --- Проверка API-ключа (теперь из кэша) ---
       API->>+Auth: @api_key_required
       Auth->>+Cache: cache.get(api_key) ?
       Cache-->>-Auth: key_obj (hit)
       Auth->>+DB: UPDATE ApiKey SET usage_count...
       Auth-->>-API: Доступ разрешен

       %% --- Логика 'save_results' (Транзакция) ---
       Note over API,DB: Начало транзакции
       API->>API: Валидация JSON (SaveResultsRequest)
       
       API->>DB: get_or_create_user() (INSERT... ON CONFLICT...)
       DB-->>API: user_obj
       
       API->>DB: get_or_create_fingerprint()
       DB-->>API: fingerprint_obj

       API->>DB: db.session.get(ResultMetadata, session_id)
       DB-->>-API: 'pending' result_obj
       
       API->>API: result.score = 95, result.raw_data = {...}

       alt Тест пройден (score >= 80)
           API->>DB: Вызов generate_document_number()
           Note right of DB: Pessimistic Lock: \n SELECT... FOR UPDATE
           DB->>DB: UPDATE DocumentCounter SET last_sequence_number += 1
           DB-->>API: doc_number (e.g., "25/11-0001")
           
           API->>API: result.document_number = "25/11-0001"
           API->>DB: INSERT Certificate (...)
       end
       
       API->>DB: db.session.commit()
       Note over API,DB: Фиксация транзакции
       DB-->>API: Commit OK

       %% --- Фаза 3: Уведомление (после Commit) ---
       group Фаза 3: Уведомление Администратора
           API->>Cache: cache.delete_memoized(get_results_api)
           
           API->>Socket: socketio.emit("update_needed", ...)
           Note right of Socket: Конфигурация message_queue\nперехватывает emit() и...
           
           Socket->>+PubSub: PUBLISH "updates" ({"type": "update_needed"})
           
           Note over PubSub,Admin: 'redis_subscriber' на ВСЕХ \nсерверах слушает канал "updates"
           
           PubSub-->>+Socket: (Сообщение получено)
           Socket->>Admin: emit("update_needed") (WebSocket push)
           Socket-->>-PubSub: (Завершение)
       end
       
       API->>API: METRICS.TESTS_COMPLETED_TOTAL.inc()
       API-->>-User: 201 Created ({"officialDocumentNumber": "25/11-0001"})
   end

2. Ключевая Функциональность и Потоки Данных (Use Cases)

2.1. Поток 1: Сбор данных (Полный "Туннель" Пользователя)

Система реализует строго регламентированную 4-этапную последовательность ("туннель"), которую проходит пользователь. Эта последовательность спроектирована для обеспечения целостности данных и предотвращения обхода этапов обучения или сбора данных.

Этап 1: index.html (Сбор данных и Идентификация)

Этап 2: index-start.html (Портал и Юридическое Предупреждение)

Этап 3: study-117.html (Обучение с Первичным Прокторингом)

Этап 4: 117-test.html (Тест и Финальный Сбор Метрик)

Этап 5: POST /api/save_results (Атомарное Сохранение на Бэкенде)

  1. get_or_create_user (атомарный INSERT ... ON CONFLICT...).
  2. ResultMetadata обновляется из pending в финальное состояние (балл, raw_data и т.д.).
  3. При успешном прохождении теста: generate_document_number() блокирует (with_for_update()) таблицу DocumentCounter для предотвращения "гонки состояний" и генерации дублирующихся номеров [cite: document.py].
  4. Создается запись в Certificate.

Этап 6: 117-test.html (Генерация PDF на Клиенте)

2.2. Поток 2: Администрирование (Модуль SPA)

Данный компонент представляет собой полноценное SPA (Single Page Application), выступающее центром управления. Его работа оркестрируется main.js [cite: main.js].

  1. Инициализация и Безопасность:
  1. Дашборд (dashboard-view):
  1. Детальное Сравнение (comparison-view):
  1. Журнал Событий (Модальное окно):
  1. Управление Настройками (settings-view):

3. Архитектура Фронтенда (Пользовательский Модуль Прокторинга)

Этот модуль функционирует как активный агент по сбору данных, а не как пассивный веб-сайт.

3.1. "Туннель" (Enforced User Flow)

Принудительная 4-этапная последовательность ("туннель") является архитектурным требованием для обеспечения целостности данных прокторинга (нельзя пройти тест, не пройдя обучение).

  1. index.html: Сбор ФИО, установка persistentId (якорь сессии).
  2. index-start.html: Портал и юридическое предупреждение о мониторинге (информированное согласие).
  3. study-117.html: Обучение + первый этап прокторинга (сбор study_started, scroll_depth для анализа вовлеченности).
  4. 117-test.html: Тестирование + второй этап прокторинга (сбор детальных поведенческих метрик).

Следует подчеркнуть, что каждый этап (начиная со 2-го) выполняет проверку localStorage, чтобы убедиться, что предыдущий был пройден. Попытка прямого доступа к study-117.html приведет к принудительному редиректу на index.html.

3.2. Система Прокторинга (117-test.html)

JS-приложение PersonalDataTestApp использует как психологические, так и технические методы для обеспечения честности теста.

Психологический Фактор (Активное Сдерживание):

Активный Мониторинг (Перехват событий):

Пассивный Сбор (Поведенческие Метрики):

3.3. Сборка Payload и Генерация PDF

  1. Бэкенд (через SystemSetting) предоставляет данные (название организации, ФИО подписантов) через PDF_SETTINGS, которые встраиваются в 117-test.html [cite: routesweb.py, 117-test.html].
  2. Бэкенд (через save_results) предоставляет officialDocumentNumber в JSON-ответе [cite: routes.py].
  3. Фронтенд (generateAndDownloadOfficialPDF) использует html2canvas для создания "снимка" скрытого HTML-шаблона (#pdf-official-blank-container) и jsPDF для упаковки его в PDF на клиенте [cite: 117-test.html].

3.4. Безопасность Фронтенда

4. Архитектура Фронтенда (Админ-Панель SPA)

Данный модуль представляет собой SPA, разработанное в соответствии с принципами разделения ответственности (SoC).

4.1. Модульная Архитектура ("Мозг")

Опишите 5-модульную архитектуру SPA, объясняя роль и обоснование каждого модуля:

  1. state.js (Сердце / Единый источник истины):
  1. api.js (Руки / Слой данных):
  1. ui.js (Лицо / Слой отображения):
  1. analysis.js (Мозг / Клиентская логика):
  1. main.js (Нервная система / Оркестратор):

4.2. Ключевые Механизмы Админ-Панели

5. Анализ Ключевых Компонентов (Backend)

5.1. Модели Данных (models.py)

5.2. API Эндпоинты (api/routes.py)

5.3. CLI-команды (commands.py)

5.4. Валидация и Метрики (result_schema.py, metrics.py)

5.5. Архитектура Безопасности (decorators.py)

  1. Кэширование в Redis: Проверенные ключи кэшируются на 5 минут (cache.set(..., timeout=300)). Подавляющее большинство запросов не вызывают ApiKey.query.
  2. Проверка прав (Scope): Проверяет is_allowed_endpoint. Это позволяет создавать ключи с гранулированными правами (например, API_KEY_FRONTEND_CLIENT может только вызывать log_event и save_results).
  3. Аудит: Атомарно инкрементирует usage_count для ключа.

5.6. Механизм Real-time Обновлений (websocket.py)

  1. api/routes.py вызывает notify_clients_of_update [cite: routes.py].
  2. Эта функция публикует (redis_client.publish("updates", ...)) JSON-сообщение в канал Redis.
  3. В каждом воркере Flask (запущенном Gunicorn) в фоне запущен redis_subscriber (из __init__.py) [cite: init.py].
  4. Этот подписчик слушает (pubsub.listen()) канал "updates".
  5. Получив сообщение (от любого воркера), он транслирует его своим локальным WebSocket-клиентам (socketio.emit(...)), которых слушает api.js админ-панели [cite: api.js].

5.7. Анализ Поведения ("Секретный Соус")

  1. Изоляция: extract_initial_stroke (отсекает паузы).
  2. Сравнение: fastDTW (математическое "совмещение").
  3. Оценка: Вычисляется взвешенная "оценка схожести": Форма (60%), Масштаб (25%), Позиция (15%).

5.8. Вспомогательные Модули (Utilities)

6. Архитектурные Решения и Рекомендации по Эксплуатации

6.1. Сильные стороны (Design Rationale)

7. Развертывание и Управление (deploy.sh & Makefile)

Этот раздел описывает слой DevOps и Управления, являющийся неотъемлемой частью системы [cite: deploy.sh].

7.1. Философия Развертывания (deploy.sh)

deploy.sh — это скрипт-оркестратор "нулевого развертывания". Его задача — превратить неконфигурированный сервер с Docker в полностью рабочую, защищенную и настроенную систему.

7.2. Процесс "Умной" Инициализации (deploy.sh)

Скрипт выполняет пошаговый процесс, подчеркивая автоматизацию и идемпотентность (безопасность повторного выполнения).

  1. Проверка Системы: Проверяет sudo, curl, openssl, docker, docker-compose.
  2. Сбор Конфигурации: Либо в интерактивном режиме, либо из env-переменных.
  3. Динамическая Генерация prod.env: Создает .env файл, генерируя SECRET_KEY с помощью openssl rand.
  4. Динамическая Генерация docker-compose.yml: Создает docker-compose.yml, описывающий 4 сервиса: postgres, redis, app (образ из ghcr.io), nginx.
  5. Динамическая Генерация nginx/nginx.conf: Создает конфигурацию Nginx на основе SERVER_NAME.
  6. Автоматизация SSL:
  1. Запускает временный Nginx.
  2. Запускает docker run ... certbot/certbot certonly --webroot... для прохождения ACME-challenge.
  3. Автоматически добавляет сервис certbot в docker-compose.yml для авто-обновления.
  4. Перезапускает Nginx с боевым Let's Encrypt сертификатом.
  1. Запуск Сервисов: Выполняет docker compose up -d.
  2. Инициализация Приложения (через docker compose exec):
  1. Генерация "Панели Управления": Создает Makefile и вспомогательные .sh скрипты.

7.3. "Панель Управления" (Makefile)

Makefile (сгенерированный deploy.sh) — это основной интерфейс (CLI) для управления развернутым приложением. Он служит абстракцией, скрывая сложные docker compose exec и pg_dump команды.

7.4. Вспомогательные Скрипты