Files
mechanic-pwa/docs/superpowers/specs/2026-05-16-premium-mechanic-design.md
T

26 KiB
Raw Blame History

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/<id>, /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/<key> на 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:

-- Осмотр
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() если поддерживается, иначе <input type="file" capture="environment">)
  • Overlay-направляющая под текущий слот (силуэт стороны авто для guidance)
  • Снять → preview → принять/перенять

Экран 6a. Vehicle Scheme (zone-навигация)

  • Интерактивная SVG-схема авто с тремя проекциями: top-down (вид сверху), боковая (left/right), интерьер
  • Каждая зона (бампер, капот, дверь, крыша, диск, лобовое и т.д.) — кликабельный hot-spot SVG <path>
  • При тапе на зону → переход в Экран 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 подтверждает: вендор использует <android.webkit.WebView> для редактора рисования повреждений. То есть наша 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 экрана.