# Premium Механик — PWA Design Doc - **Дата:** 2026-05-16 - **Статус:** Draft, ожидает review владельца - **Владелец:** vladtechno@gmail.com (PremiumPark) - **Родительский проект:** PremiumDriverApp - **Экосистема:** Premium CRM (TaxiDashboard) на crm.pptaxi.ru - **Предшественник:** реверс-разведка приложения «Механик» NaughtySoft (см. `01-static-analysis.md` + перехват трафика) --- ## 1. Контекст и цель Сотрудники-механики PremiumPark сейчас работают через приложение «Механик» вендора NaughtySoft (`com.naughtysoft.ttc`, Xamarin/.NET). Все данные осмотров уходят в 1С вендора, доступ к ним из PremiumCRM невозможен без прохода через вендорские API. Через разведку (см. `02-network-capture.md`) подтверждён формат backend-API вендора (REST на `api.ttcontrol.naughtysoft.ru`). Эндпоинты осмотров (`/api/vehicletechinspection/byvehicle`, `/api/vehiclestate/history`, `/api/image/`, `/api/inspectionvideo`) подтверждают, что **модель данных осмотров с координатами повреждений и фото уже существует и нам полностью видна**. **Цель проекта:** построить **собственное** приложение «Premium Механик» (PWA), которое: 1. Полностью заменяет вендорское приложение для сотрудников-механиков. 2. Хранит данные осмотров **в нашей PostgreSQL** в PremiumCRM, не отправляя их к вендору. 3. Поддерживает основные кейсы: съёмка фото авто, обводка повреждений на фото, метки повреждений с координатами, сравнение с прошлым осмотром при приёмке. 4. Развёртывается без RuStore / App Store — деплой = `git push`. **Долгосрочная цель:** отвязать парк от 1С вендора, начав с самой ценной модели данных (осмотры с фото). ## 2. Цели и не-цели ### Цели 1. Делать осмотр авто: 4-8 фото по сторонам, метки и обводки повреждений на каждой фото. 2. При приёмке машины обратно от водителя — показать прошлые повреждения для сравнения «было / стало». 3. Все данные осмотра хранятся в PostgreSQL PremiumCRM (новая БД-схема), без участия вендора. 4. Доступ для авторизованных сотрудников-механиков (через существующий auth PremiumCRM). 5. Работает на Android-планшетах механиков, на iPad/iPhone, на десктопе — без раздельных сборок. ### Не-цели - Не делаем приложение для водителя в рамках этого подпроекта (отдельная фаза). - Не интегрируем фактическую интеграцию обратно в 1С вендора (наоборот — уходим от неё). - Не делаем нативные APK / IPA в первой версии. Только PWA. Возможен Capacitor-fallback в фазе 3 при необходимости. - Не делаем offline-first в MVP (механик работает с WiFi в парке). Service Worker — задел на будущее. - Не делаем real-time чат / уведомления через push на iOS (сложно, неприоритетно). - Не импортируем существующие осмотры из вендора в первой версии (отдельная задача миграции данных). ## 3. Технологический стек ### Frontend | Слой | Выбор | Обоснование | |---|---|---| | Build/Dev | **Vite + TypeScript** | Быстрая итерация, типизация. Стек, уже знакомый команде. | | UI Framework | **React 18** | Команда уже работает с React (SuperLanding на Astro + React islands). | | UI Library | **shadcn/ui + Tailwind CSS** | Совпадает с design preferences (cream palette, Inter only, без жёлтого, без emoji). | | State Client | **TanStack Query (React Query)** | Кэширование fetches, optimistic updates, retries. | | Local State | **Zustand** | Минималистичный store без boilerplate. | | Routing | **React Router 7** | Standard в React экосистеме. | | Forms | **React Hook Form + Zod** | Типизированная валидация, лёгкий контроль. | | Canvas/Annotation | **Konva.js + react-konva** | Лучший выбор для photo annotations: layers, touch, gestures, undo/redo. | | HTTP | **Native fetch + ky или axios** | TBD на старте. | | PWA | **vite-plugin-pwa** | Service Worker, web manifest, install prompt — из коробки. | | QR-сканер | **html5-qrcode** | Для сканирования госномера-стикера машины. | ### Backend | Слой | Выбор | |---|---| | Расширение | Новый модуль `app/mechanic/` в FastAPI PremiumCRM | | Базовый URL | `/api/v1/mechanic/*` | | ORM | SQLAlchemy (используется в TaxiDashboard) | | Миграции | Alembic | | Auth | Существующий PremiumCRM (JWT cookie + bcrypt PIN) — переиспользуем | | Permissions | Существующая dept-based система permissions (см. memory: `project_taxidashboard_permissions`) | | Файловое хранилище | **MinIO** на 100.64.0.12 (контейнер `taxi-minio`). Bucket **`pp-inspections`** (уже создан). Endpoint `s3.pptaxi.ru` (HTTPS), admin UI `s3-admin.pptaxi.ru`. Креды в `/opt/sites/taxi-dashboard/minio.env`. | | S3 client | **`aiobotocore`** — переиспользуем существующую интеграцию из TaxiDashboard (она же используется для фото осмотров уже). | | Upload-схема | **Pre-signed POST** — фронтенд льёт фото напрямую в MinIO, минуя FastAPI. Backend выдаёт presigned URL + поля. | | Read-схема | Существующий nginx-proxy `/media/` на VDS (уже работает для других фото TaxiDashboard) ИЛИ pre-signed GET с TTL 1 час. Решим на этапе implementation. | | Image-processing | Pillow для thumbnails и EXIF strip — запускается celery-task'ом после `confirm` upload'а. | | Дополнительно | **Только MinIO**, без локального дубля на FS (решение владельца). Расхождение с существующим dual-write-паттерном в TaxiDashboard оправдано тяжестью медиа осмотров (10-20 фото на осмотр) — забивать диск VDS дублями нецелесообразно. | ### Развёртывание | Хост | Назначение | |---|---| | 100.64.0.12 (PremiumCRM VDS) | Backend + DB + статика фото | | Caddy | Reverse proxy + TLS + raw уролы для фото | | Поддомен | `mechanic.pptaxi.ru` либо `crm.pptaxi.ru/mechanic/` (TBD, см. open question) | ## 4. Модель данных Новые таблицы в существующей `taxi_dashboard` PostgreSQL: ```sql -- Осмотр CREATE TABLE inspections ( id BIGSERIAL PRIMARY KEY, vehicle_id BIGINT NOT NULL REFERENCES vehicles(id), type VARCHAR(32) NOT NULL, -- 'handover'|'return'|'periodic'|'ad-hoc' performed_by INTEGER NOT NULL REFERENCES users(id), driver_id INTEGER REFERENCES users(id), started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), finished_at TIMESTAMPTZ, status VARCHAR(16) NOT NULL DEFAULT 'in_progress', -- 'in_progress'|'completed'|'cancelled' notes TEXT, prev_inspection_id BIGINT REFERENCES inspections(id), -- ссылка на предыдущий осмотр того же авто meta JSONB DEFAULT '{}'::jsonb ); CREATE INDEX idx_inspections_vehicle_started ON inspections(vehicle_id, started_at DESC); -- Фото осмотра CREATE TABLE inspection_photos ( id BIGSERIAL PRIMARY KEY, inspection_id BIGINT NOT NULL REFERENCES inspections(id) ON DELETE CASCADE, side VARCHAR(16) NOT NULL, -- 'front'|'rear'|'left'|'right'|'interior'|'vin'|'odometer'|'wheel-fl'|'wheel-fr'|'wheel-rl'|'wheel-rr'|'free' slot_index INT DEFAULT 0, storage_key TEXT NOT NULL, -- путь относительно /opt/premiumcrm/uploads/ annotated_key TEXT, -- путь к фото с canvas-обводкой (опц.) thumb_key TEXT, width INT, height INT, bytes BIGINT, taken_at TIMESTAMPTZ DEFAULT NOW(), display_order INT DEFAULT 0 ); -- Метка повреждения (на фото и/или на схеме авто) CREATE TABLE damage_markers ( id BIGSERIAL PRIMARY KEY, inspection_id BIGINT NOT NULL REFERENCES inspections(id) ON DELETE CASCADE, photo_id BIGINT REFERENCES inspection_photos(id), -- NULL если только на схеме side VARCHAR(16), -- сторона авто x DECIMAL(6,4), -- normalized 0..1 в координатах фото или схемы y DECIMAL(6,4), polygon JSONB, -- опционально: координаты обведённого контура [{x,y}, ...] damage_type VARCHAR(32), -- 'scratch'|'dent'|'paint'|'crack'|'missing'|'rust'|'glass' severity VARCHAR(16), -- 'cosmetic'|'minor'|'moderate'|'severe' description TEXT, carried_over_from_id BIGINT REFERENCES damage_markers(id), -- если унаследовано от прошлого осмотра resolved BOOLEAN DEFAULT FALSE, -- было ли устранено created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_damage_markers_inspection ON damage_markers(inspection_id); ``` **Ключевая идея для дифа «было/стало»:** - При создании return-inspection backend копирует все `damage_markers` из `prev_inspection_id` со ссылкой `carried_over_from_id`. Механик может либо подтвердить «есть» (оставить), либо отметить «устранено» (`resolved=true`), либо добавить новые маркеры. На фронте отображается две группы: «было раньше» (полупрозрачные) + «новые» (яркие). ## 5. API контракт Все endpoints под `/api/v1/mechanic/`. Auth — через существующий JWT cookie PremiumCRM. Permission gate: `mechanic:inspect` (новое право в матрице, добавляется в модуль permissions). ### Машины ``` GET /vehicles?q=<гос-номер|VIN>&limit=20 → [{id, license_plate, vin, make, model, year, current_driver, last_inspection_at}] GET /vehicles/{id} → детали авто + список последних осмотров GET /vehicles/{id}/last-inspection → последний завершённый осмотр (для дифа при приёмке) ``` ### Осмотры ``` POST /inspections body: {vehicle_id, type, driver_id?} → {id, ...} создан в статусе in_progress; если type='return', копирует markers из prev GET /inspections/{id} → полная карточка с photos + markers + meta PATCH /inspections/{id} body: {status?, notes?, finished_at?} POST /inspections/{id}/photos/upload-url body: {side, slot_index, content_type, size} → {photo_id, presigned_url, fields, storage_key, expires_at} # фронт льёт сам файл напрямую в MinIO через presigned_url POST /photos/{photo_id}/confirm body: {actual_size?, width?, height?} → {id, storage_key, thumb_key, status: 'processing'|'ready'} # после confirm backend ставит celery-task на thumbnail и EXIF strip POST /inspections/{id}/markers body: {photo_id?, side, x, y, polygon?, damage_type, severity, description} → {id, ...} PATCH /markers/{id} body: {damage_type?, severity?, description?, polygon?, resolved?} DELETE /markers/{id} POST /photos/{id}/annotated/upload-url body: {content_type, size} → {presigned_url, fields, storage_key, expires_at} # аналогично: фронт льёт annotated-версию напрямую в MinIO POST /photos/{id}/annotated/confirm → {id, annotated_key, status: 'ready'} ``` ### Чтение фото ``` GET /photos/{id} → 302 Redirect на pre-signed S3 URL (TTL 1 час) GET /photos/{id}/thumb → 302 Redirect на pre-signed S3 URL thumbnail GET /photos/{id}/annotated → 302 Redirect на pre-signed S3 URL annotated ``` ### Меня (текущий механик) ``` GET /me → {id, name, dept, permissions: [...], avatar_url} ``` ## 6. UI экраны и поток ### Экран 1. Login PIN-вход в PremiumCRM (как уже работает в TaxiDashboard). ### Экран 2. Главная — выбор машины - Поле поиска по госномеру/VIN - Кнопка «Сканировать QR» (стикер на лобовом стекле машины с госномером) - Список последних 10 машин, с которыми работал этот механик - На каждой карточке: гос-номер, модель, водитель, дата последнего осмотра ### Экран 3. Карточка авто - Резюме (гос-номер, VIN, модель, цвет, год) - Текущий водитель (если в аренде) - **Кнопки:** «Передача водителю» (handover) | «Приёмка от водителя» (return) | «Плановый осмотр» (periodic) | «Свободный осмотр» (ad-hoc) - **Прошлые осмотры** — лента (превью фото + метки) ### Экран 4. Новый осмотр — список «слотов» фото Карточка-grid: 4 обязательных + 4 опциональных: - Перед, Зад, Левый, Правый (обязательные) - VIN, Одометр, Салон, Свободное фото - Каждый слот: пустой → камера UI; заполнен → миниатюра + индикатор количества меток - В режиме `return` каждая карточка слота показывает «было N меток, добавь новые / подтверди / отметь устранённые» ### Экран 5. Камера - Полноэкранный preview (через `getUserMedia()` если поддерживается, иначе ``) - Overlay-направляющая под текущий слот (силуэт стороны авто для guidance) - Снять → preview → принять/перенять ### Экран 6a. Vehicle Scheme (zone-навигация) - Интерактивная SVG-схема авто с тремя проекциями: top-down (вид сверху), боковая (left/right), интерьер - Каждая зона (бампер, капот, дверь, крыша, диск, лобовое и т.д.) — кликабельный hot-spot SVG `` - При тапе на зону → переход в Экран 5 (Камера) с предзаполненным `side`/`slot_index` - Поверх схемы — полупрозрачные метки прошлых повреждений (для return-режима): красные точки на тех зонах, где были повреждения - Прогресс осмотра: подсветка зон, у которых уже есть фото (зелёный outline) ### Экран 6b. Annotation Editor (главная фишка) - Фото на весь экран, поверх — Konva canvas - Tool panel: - 🔘 **Маркер** — тап создаёт точку-метку - ✏️ **Обводка** — рисуем замкнутый контур (полигон) вокруг повреждения - ↩️ Undo / 🗑️ Erase - 🎨 Тип/severity выбор (4 типа × 4 severity) - При тапе на маркер — modal с типом, severity, описанием - В `return`-режиме старые маркеры показываются полупрозрачными; тап → «есть/устранено» ### Экран 7. Обзор осмотра - Карточка-grid всех фото + список всех маркеров + общие notes - Кнопка «Завершить осмотр» → finished_at, status=completed ### Экран 8. Подтверждение (опц.) - Подпись водителя (canvas) — опционально - Сохранение, переход на главную ## 7. Архитектура frontend ``` src/ ├── api/ │ ├── client.ts # настройка fetch/axios + auth interceptor │ ├── inspections.ts # endpoints через TanStack Query │ ├── vehicles.ts │ └── photos.ts ├── components/ │ ├── ui/ # shadcn-сгенерированные │ ├── camera/ │ │ ├── CameraCapture.tsx │ │ └── PhotoSlot.tsx │ ├── annotation/ │ │ ├── AnnotationCanvas.tsx # Konva-based │ │ ├── ToolPalette.tsx │ │ └── MarkerModal.tsx │ ├── vehicle-scheme/ │ │ ├── VehicleScheme.tsx # SVG interactive scheme │ │ ├── schemes/ # SVG-файлы по типу кузова (sedan, hatch, suv) │ │ │ ├── sedan-top.svg │ │ │ ├── sedan-side.svg │ │ │ └── interior.svg │ │ └── HotSpot.tsx # переиспользуемая зона-кнопка │ └── shared/ ├── pages/ │ ├── Login.tsx │ ├── Home.tsx │ ├── VehicleCard.tsx │ ├── InspectionStart.tsx │ ├── InspectionEditor.tsx │ └── InspectionReview.tsx ├── store/ # Zustand store(s) │ ├── inspectionDraft.ts │ └── auth.ts ├── lib/ │ ├── pwa.ts # SW registration │ └── coords.ts # norm/denorm coords ├── App.tsx └── main.tsx ``` ## 8. Архитектура backend (модуль в PremiumCRM) ``` app/mechanic/ ├── __init__.py ├── router.py # /api/v1/mechanic/ роуты ├── models.py # SQLAlchemy: Inspection, InspectionPhoto, DamageMarker ├── schemas.py # Pydantic ├── service.py # бизнес-логика (start_inspection, finalize, copy_markers, ...) ├── storage.py # сохранение файлов на VDS + thumbnails ├── deps.py # auth + permission deps └── migrations/ └── 20260516_mechanic_init.py # Alembic ``` ## 9. Фазы реализации ### Фаза 1. MVP — Skeleton (3-4 нед) Только основной flow без annotation, для первичного теста UX: - Backend: модуль `mechanic`, миграции, endpoints вэхикулов и осмотров, фото upload (без annotation) - Frontend: Login → Home → Vehicle → Inspection (создать, выбрать тип, добавить фото в 4 слота, добавить просто маркер-точку без сложной обводки) → Review → Complete - Дифф с прошлым: список маркеров прошлого осмотра, чекбоксы «есть/устранено» ### Фаза 2. Annotation Editor (+2-3 нед) - Konva-based canvas-overlay с инструментами обводки (polygon) - Сохранение annotated-фото отдельным URL - Старые маркеры/обводки полупрозрачно поверх новой фотографии для сравнения ### Фаза 3. Polish (+1-2 нед) - Service Worker + offline queue (фото снимаются на устройство, загружаются по WiFi) - QR-сканер стикеров - Push-уведомления для админа (через web push с FCM-фолбэком или Telegram-бота) - Дашборд статистики в админ-части PremiumCRM ### Фаза 4 (опционально). Capacitor wrapper - Если в production столкнёмся с ограничениями iOS Safari camera или offline - Тот же codebase → native APK + IPA - ~1-2 недели ## 10. Связь с разведкой - Endpoints, обнаруженные у вендора (`api.ttcontrol.naughtysoft.ru/api/vehicletechinspection/byvehicle` и пр.), могут быть использованы **отдельно** для **миграции данных** — одноразовый импорт прошлых осмотров вендора в нашу базу. Это отдельный sub-project (не входит в MVP). - В первой версии работаем только с НОВЫМИ осмотрами, создаваемыми в PremiumMechanic — старые остаются в системе вендора и используются для справки до момента миграции. ## 11. Решения и открытые вопросы ### Решения (по итогам ревью владельца) | # | Решение | |---|---| | Поддомен | **`mechanic.pptaxi.ru`** (отдельный поддомен). DNS уже настроен владельцем. На VDS 100.64.0.12 в Caddyfile добавляется блок с TLS через ACME. | | Подпись водителя | **Не нужна** в MVP. Из спеки исключена. | | 2D-схема авто | **Нужна обязательно с MVP.** Делаем SVG-схему сами: top-down + боковая проекция + интерьер. Не привязываемся к ассетам вендора (в APK schemes как раздельных файлов нет, у них всё через WebView + либо embedded в Xamarin DLL, либо отдельная фотография крупным планом). У нас SVG-первый подход с zone-based hot-spots. | ### Замечания по разведке вендорского редактора - Layout `vehicleinspectiondetaildrawinglayout.xml` подтверждает: вендор использует `` для редактора рисования повреждений. То есть **наша PWA-стратегия = технически тот же подход, что у вендора**, только без обёртки Xamarin. - Заголовок WebView в layout = «Левая дверь» — у вендора **zone-based** детализация (кнопка → крупный план зоны → редактирование). - ObjectID формата `660e7abfd13231f1dd6e8a4d` в `vehicleId` доказывает, что у вендора на бэке **MongoDB**, а не 1С. Имя `taksi.0nalog.com:1703` (виденное в Driver-приложении) — отдельный 1С-узел, не основной storage. Это означает: имитировать «1С-структуру» нам не надо — у вендора и так нет 1С на ключевом пути данных осмотров. ### Остающиеся open questions 1. **Permissions:** добавить новое право `mechanic:inspect` или ограничиться существующим `dept=Тех. служба`? — выбор на этапе implementation. 2. **Storage:** решено использовать **MinIO**, **только** MinIO без локального FS-дубля (разрешено отклониться от dual-write паттерна для тяжёлых медиа). Параметры: bucket `pp-inspections`, endpoint `s3.pptaxi.ru` HTTPS, креды в `/opt/sites/taxi-dashboard/minio.env`. Существующая `aiobotocore`-интеграция переиспользуется. Остаётся **операционно**: настроить CORS на bucket для allowed origins (`https://mechanic.pptaxi.ru`, `https://crm.pptaxi.ru`, dev `http://localhost:5173`) — задача в плане реализации. 3. **Связь с CheckAuto:** опционально показывать в карточке машины автоматические проверки водителя — после MVP. 4. **Импорт истории осмотров от вендора:** через VendorBridge sub-project — после MVP. ## 12. Следующий шаг После утверждения этой спеки: 1. Создать подробный план реализации Фазы 1 через `writing-plans` skill. 2. Сгенерировать миграции БД + scaffold бэкенд-модуля. 3. Скаффолд React-проекта в `mechanic-pwa/frontend/` с базовой страницей логина. 4. На итерациях по 2-3 экрана.