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

Gatehouse — testing guide

Інструкція для розробника / QA / нового користувача — як перевірити, що Gatehouse plugin зібраний правильно і всі 5 спринтів працюють end-to-end. Не плутати з user-guide.md (для оператора, без термінів JWT / WS / migrate).

0. Передумови — без цього нічого не запрацює

Що Як перевірити Якщо ні
Postgres dev-БД на localhost:5433 живий docker compose -f docker-compose.dev.yml ps postgres показує healthy docker compose -f docker-compose.dev.yml up -d postgres
Backend venv створений ls backend/venv/Scripts/python.exe cd backend && python -m venv venv && venv/Scripts/python.exe -m pip install -r requirements.txt
Міграції Gatehouse + HRM + FortNet застосовані python manage.py showmigrations gatehouse hrm fortnet_scud python manage.py migrate
AppStore-products створені python manage.py shell -c "from shop.models import Product; print(Product.objects.filter(code='appGatehouse').exists())"True python manage.py seed_shop
Seed-дані Gatehouse v5 створені python manage.py shell -c "from gatehouse.models import GatePoint; print(GatePoint.objects.filter(tenant__code='demo').count())"4 python manage.py seed_gatehouse
Frontend dev-сервер на localhost:5173 curl -sf http://localhost:5173 cd frontend/erp && npm run dev
EasyOCR встановлений (Sprint 2 + далі) cd backend && venv/Scripts/python.exe -c "import easyocr; print('OK')" pip install easyocr (heavy ~600MB)
html5-qrcode у frontend (Sprint 3) grep html5-qrcode frontend/erp/package.json cd frontend/erp && npm install html5-qrcode
device-bridge venv (Sprint 2) ls device-bridge/venv/Scripts/python.exe cd device-bridge && python -m venv venv && venv/Scripts/python.exe -m pip install -r requirements.txt

Швидкий старт усього стенду:

# Термінал 1 — backend + ERP (через VS Code task або cli)
node launcher-dev.js

# Термінал 2 — Gatehouse hardware bridge (Sprint 2+)
node launcher-bridge.js

Лаунчери опубліковані як VS Code tasks: Ctrl+Shift+PTasks: Run TaskLauncher: Dev / Launcher: Bridge.


Sprint 1 — Universal core (manual flow)

Що перевіряємо: усі 3 сценарії з §2 плану проходяться руками; net_weight рахується правильно; кнопки розблоковуються коли треба.

Кроки

  1. Відкрий http://localhost:5173/gatehouse — побачиш SectionDashboard з 4 групами (Master Data, Transaction Data, Reports, Processes).
  2. Master Data → Gate Points — список з 4 рядків:
  3. gate_main (mixed)
  4. gate_personnel (personnel_turnstile)
  5. gate_weighbridge (weighing_station)
  6. checkpoint_yard_1 (vehicle_gate)
  7. Master Data → Equipment — 7 одиниць обладнання, з них видно колонку driver (file_scale_generic, anpr_webcam_browser, ...).
  8. Transaction Data → Weighing Tickets — 5+ записів від попередніх sprints (3 closed + 1 open + 1 needs_review зі Sprint 4 + 2 з Sprint 1).
  9. Transaction Data → Gate Events — 14+ подій з minulого тижня + з seed-flow.
  10. Клацни на будь-який ticket зі статусом weighed_in (відкритий) або зайди у Transaction Data → Weighing Tickets → New:
  11. Заповни прохідну gate_weighbridge, дату.
  12. Клацни «Створити талон» (синя кнопка справа).
  13. На URL → /.../<id> форма перезавантажилась.
  14. Кнопки Брутто / Тара / Закрити тепер активні (Tooltip-и на disabled-кнопках також показують підказки).
  15. Введи у поле «Брутто (заїзд), kg»: 25400 → клацни кнопку Брутто → статус → weighed_in, у Stepper підсвічений 2-й крок.
  16. Введи у поле «Тара (виїзд), kg»: 12000 → клацни Тара → статус → weighed_out, Stepper → крок 3.
  17. Клацни Закрити талон → статус → closed, Stepper → крок 4. Перевір net_weight = 13400 kg у блоці «Нетто (computed)».

DoD ✅

  • [ ] 4 GatePoint різних purpose бачимо у списку.
  • [ ] Можна створити WeighingTicket з нуля → пройти Брутто → Тара → Закрити → net правильний.
  • [ ] Tooltip-и на заблокованих кнопках пояснюють чому.
  • [ ] Status Stepper підсвічує правильний крок.

Якщо щось зламалося

Симптом Причина / фікс
entity not found у MasterDataPage seed не пройшов або EntityRegistry.register загубився — перезапусти backend
Кнопки disabled на існуючому ticket'і Tooltip скаже чому: closed/posted ticket не редагується (як у фактичних бух-документах)
net_weight не оновлюється Model.save() має recompute — якщо ні, переглянь weight_in/weight_out decimal precision

Sprint 2 — Hardware bridge + WebSocket live-feed + EasyOCR webcam

Що перевіряємо: обладнання з'являється у Live-feed без перезавантаження; webcam ANPR розпізнає номер з фото зі смартфона з ≥1 з 3 спроб у нормальному світлі.

2.1 Bridge + scale-emulator

  1. Лаунчер: node launcher-bridge.js (або VS Code → Launcher: Bridge).
  2. У логах побачиш:
    [bridge] starting device-bridge for gate_point_id=1, 2 equipment
    [bridge] [file_scale_generic] polling C:\weights\current.txt every 2.0s (deadband 1.0 kg)
    [bridge] [card_reader_emulator] watching C:\cards\latest.txt every 2.0s
    [scale]  scale_emulator: writing to C:/weights/current.txt every 2s; pattern=in_loaded_out_empty
    
  3. Кожні ~2 секунди bridge POSTить event у backend:
    [bridge] HTTP Request: POST http://localhost:8000/api/v1/gatehouse/equipment-events/ "HTTP/1.1 200 OK"
    [bridge] drained 1/1 pending events
    

2.2 Live-feed на Dashboard

  1. Відкрий http://localhost:5173/gatehouse/gatehouseprocesses/gatehouseDashboard.
  2. У header справа — зелений Badge WS онлайн.
  3. Секція Live-feed обладнання показує події з kind=scale приблизно раз на 2-5 секунд (під час фаз ramp циклу 60с; у steady-фазах буде тиша через deadband_kg=1.0).
  4. Картки прохідних показують Обладнання: 3 для gate_main, 2 для gate_weighbridge, тощо.

2.3 Webcam ANPR

  1. http://localhost:5173/gatehouse/gatehouseprocesses/webcamAnprTester.
  2. Дозволь доступ до камери.
  3. На смартфоні відкрий фото номера в форматі UA (AA0000AA, AA00000, або AAA0000). Можеш загрузити з Google Images "украинский номерной знак".
  4. Обери gate_main у dropdown «Прохідна».
  5. Клацни «Зробити фото» → 5-15 секунд (перший виклик завантажує EasyOCR-модель ~80 MB; наступні швидкі).
  6. У правій панелі — plate_display (наприклад KA 1234 AB), confidence: 91%, на webcam-overlay видно зелений bbox з номером.
  7. Клацни «Підтвердити» → toast Подію надіслано: KA1234AB.
  8. Перейди назад на Dashboard → у Live-feed побачиш свіжу anpr_camera-event.
  9. Включи Auto mode + Interval 2с + Min confidence 0.8 → тримай фото у кадрі → раз на 2с auto-push події (з дедупом на той самий plate).

DoD ✅

  • [ ] Bridge стартує, тримає 2 driver-таски, drains outbox у backend.
  • [ ] Indicator WS онлайн зелений у dashboard.
  • [ ] Scale events з'являються у Live-feed з ~2с інтервалом.
  • [ ] Webcam ANPR розпізнає plate ≥1 з 3 спроб у нормальному світлі.
  • [ ] Auto mode деуплює — 5 секунд показу однієї plate = 1 push, не 50.

Якщо щось зламалося

Симптом Причина / фікс
Bridge: 401 Unauthorized JWT протух (60-хв lifetime) — node launcher-bridge.js оновлює його при старті
Live-feed порожній WS не зʼєднався — F12 console шукай WebSocket onclose code=4001/4002/4003; перевір чи в useAuthStore.user.tenant_id == path tenant_id
EasyOCR endpoint висить Перший виклик — model download ~80MB. Логи backend покажуть Loading EasyOCR reader.... Чекай ~30 секунд, не клікай знову
EasyOCR 503 з install_hint cd backend && venv/Scripts/python.exe -m pip install easyocr
OCR не розпізнає UA-номер Спробуй: матовий папір з більшим plate, кращий світ, кадр під невеликим кутом. Sprint 5+ → можна замінити EasyOCR на FastALPR

Sprint 3 — Vehicle gate scenario (ANPR + QR labels + checkpoint timeline)

Що перевіряємо: end-to-end від webcam-фото до закритого vehicle event з 4 checkpoint'ами у timeline.

Кроки

  1. Operator screen: http://localhost:5173/gatehouse/gatehouseprocesses/gateOperator → обери gate_main (mixed-purpose, тут vehicles).
  2. У іншій вкладці — webcamAnprTester (з Sprint 2). Покажи фото номера, що вже є у seed:
  3. AA1234BB (Volvo FH 25-тонник), AA5678CC (MAN), BC4567CC (Mercedes), BC1122DD (Renault), AA9988EE (Iveco).
  4. Або номер, якого немаєregister_vehicle_gate_event авто-створить новий Vehicle(name='Авто XX0000XX', ownership_type='hired', description='Auto-created from ANPR gate detection.').
  5. У Operator screen з'явиться картка з:
  6. Великий ff="monospace" plate.
  7. Vehicle name + driver name (якщо є).
  8. QR UID (нащо вище, поки що пустий — буде заповнений після accept).
  9. Клацни Прийняти + друк:
  10. POST /gate-events/{id}/accept/ → status draft → registered, qr_label_uid згенерувався.
  11. У новій вкладці автоматично відкривається vehicle-gate-pass.pdf (A6 landscape з plate + QR + meta).
  12. PDF можна:
  13. Зберегти на локальний диск.
  14. Відобразити на іншому екрані (другий монітор / смартфон).
  15. Mobile checkpoint scanner: інша вкладка → http://localhost:5173/gatehouse/gatehouseprocesses/checkpointScanner/checkpoint_yard_1:
  16. Обери пост checkpoint_yard_1.
  17. Клацни Старт сканера → дозволь webcam.
  18. Направ на QR з PDF (з кроку 6).
  19. Toast: Чекпойнт записано: AA1234BB @ checkpoint_yard_1.
  20. У правій панелі сканера — recent scan з кнопкою Маршрут.
  21. Клацни Маршрут → відкривається vehicleTrace/<event_id>:
  22. Header: plate великий, НА ТЕРИТОРІЇ помаранчевий бейдж, На території: +35 хв.
  23. Timeline: Заїзд на територію (синій) → Чекпойнт checkpoint_yard_1 (cyan) → ... з time deltas.
  24. Повернись у webcam-tester → показ того ж номера, але цього разу натискай --direction out (треба додати у URL? — або вкладка emulator з прапорцем direction).
    • Альтернативно — використай python device-bridge/emulators/anpr_cli.py --plate AA1234BB --gate-point 1 --direction out --backend http://localhost:8000 --token <JWT>.
  25. У VehicleTrace → автоматично з'явиться Виїзд з території, статус → ЗАВЕРШЕНО (зелений).

DoD ✅

  • [ ] ANPR-event для seeded vehicle → operator card з готовою vehicle info.
  • [ ] Accept → PDF з QR відкривається у новій вкладці без помилок.
  • [ ] PDF читається сканером (можеш відкрити PDF на смартфоні і направити webcam).
  • [ ] Checkpoint scan додає GateCheckpoint у timeline.
  • [ ] VehicleTrace показує open-visit → close переключає на ЗАВЕРШЕНО.

Якщо щось зламалося

Симптом Причина / фікс
PDF дає 404 / 500 PrintRegistry.get('vehicle-gate-pass') не знайшов template — перевір backend/gatehouse/print_templates/vehicle-gate-pass/base.html існує
PDF друкується, але без QR qr_label_uid був пустим до accept — клацни Reject + Accept ще раз; у seeded events QR вже є
Сканер не відкриває камеру HTTPS required для getUserMedia на не-localhost. Use localhost:5173 явно
Сканер не декодить QR Утримуй стабільно 2-3с, очисти екран від відблисків. html5-qrcode логи у F12 console
Checkpoint scan дає 404 QR з seeded events має qr_label_uid != ''; нові events отримують його лише після accept

Sprint 4 — Weighing scenario (ANPR + scale correlator + GoodsReceipt bridge)

Що перевіряємо: повний end-to-end від webcam-фото до проведеного GoodsReceipt без жодного ручного вводу ваги.

4.1 Correlator pairing

  1. У launcher-bridge: scale-emulator має писати у C:/weights/weighbridge.txt (за замовчуванням пише у current.txt для gate_main). Підправ через флаг або вручну запусти:
    cd device-bridge && venv/Scripts/python.exe emulators/scale_emulator.py \
        --file C:/weights/weighbridge.txt --pattern static --value 25000
    
  2. У device-bridge.toml додай ще один equipment:
    [[equipment]]
    driver = "file_scale_generic"
    config = { file_path = "C:/weights/weighbridge.txt", poll_interval = 2.0, deadband_kg = 1.0 }
    
    Або просто змінь існуючий — поки що використаємо одну вагу.
  3. Тимчасово зміни bridge gate_point_id у toml на 3 (gate_weighbridge), щоб scale events ішли на вагову (correlator слухає тільки weighing_station / mixed).
  4. Перезапусти Launcher: Bridge. Bridge тепер постить scale events на gate_weighbridge.
  5. Operator screen вагової: http://localhost:5173/gatehouse/gatehouseprocesses/weighbridgeOperator → обери gate_weighbridge.
  6. У картці «Останнє зчитування ваги» через 2-5с з'явиться 25000.00 kg.
  7. У іншій вкладці → webcamAnprTester → обери gate gate_weighbridge → фото номера AA1234BB.
  8. Через ~2 секунди backend correlator знайде пару → у списку «Відкриті талони» з'явиться новий ticket з weight_in=25000, статус weighed_in.

4.2 Tare + close

  1. Перепиши scale-emulator: --value 12000 (порожнє авто).
  2. У webcam-tester → ще раз показ того ж номера → correlator знайшов open ticket → weight_out=12000, direction_pattern=in_loaded_out_empty, статус → closed, net=13000 kg.
  3. У списку «Відкриті талони» → ticket зник (бо closed). Він з'явився у блоці «Готові до приходу».

4.3 GR bridge

  1. Клацни на ticket у блоці «Готові до приходу» → відкриється форма з Stepper'ом на 4-му кроці (Закрито).
  2. Кнопка Створити прихід (GoodsReceipt) активна (teal). Клацни.
  3. У новій вкладці відкривається GoodsReceipt:
  4. 1 рядок: cargo_item (Зерно з seed) × 13000 kg (або 13 t якщо unit=т).
  5. FK weighing_ticket → ticket з кроку 1.
  6. Повернись у форму ticket → Stepper → 5-й крок (Прихід #N), kнопка Створити прихід замінилась на teal Alert з посиланням.

DoD ✅

  • [ ] Correlator пов'язує ANPR + scale у вікні 30с.
  • [ ] WeighingTicket створюється/закривається без жодного ручного вводу ваги.
  • [ ] direction_pattern auto-derives (брутто > тара → in_loaded_out_empty).
  • [ ] GR створюється з рядком cargo_item × net_weight, FK назад.
  • [ ] Idempotent — повторний клік Створити прихід повертає той самий GR, не дубль.

Якщо щось зламалося

Симптом Причина / фікс
Correlator не пов'язує Перевір gate_point.purpose ∈ ('weighing_station', 'mixed'). Buffer per-process → один Daphne worker, no Redis у Sprint 4
Ticket створюється з status needs_review Suspicious weight (weight≤0) або vehicle mismatch — подивись note поле; виправ у формі вручну
GR creation 400 Перевір що ticket має cargo_item + partner + gate_point.organization
GR створюється, але порожній Item.base_unit має бути unit, що знає система; default = kg. Якщо unit=т, qty конвертується в тонни

Sprint 5 — Personnel turnstile + FortNet integration + ContainerHub bridge

Що перевіряємо: AttendanceLog приходить з обох каналів (own + FortNet); recompute_days_worked працює; ContainerHub bridge створює GateTransaction (на ContainerHub-tenant).

5.1 Власний канал турнікета

  1. Card emulator через лаунчер: node launcher-bridge.js --card AABBCC01.
  2. У логах:
    [card] card_emulator [1/1]: wrote AABBCC01 to C:\cards\latest.txt
    [bridge] [card_reader_emulator] new UID: AABBCC01
    [bridge] HTTP Request: POST .../equipment-events/ "HTTP/1.1 200 OK"
    
  3. У DB перевір AttendanceLog з'явився:
    cd backend && venv/Scripts/python.exe -c "
    import django, os
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eswf.settings.development')
    django.setup()
    from hrm.models import AttendanceLog
    for l in AttendanceLog.objects.filter(tenant_id=6, source='gatehouse').order_by('-date'):
        print(l)
    "
    
    Очікувано: 1+ рядок з source='gatehouse', is_open=True.

5.2 FortNet канал

  1. Перевірка налаштувань: http://localhost:5173/gatehouse/gatehouseprocesses/fortnetIntegration:
  2. csv_path: C:/eswf/backend/integrations/fortnet_scud/seed_data/access_log_2026_04.csv (з seed v5).
  3. last_synced_event_id: пустий (cursor ще не рухався).
  4. is_enabled: Увімкнено (зелений Badge).
  5. last_log: «ніколи».
  6. Клацни «Синхронізувати зараз» → backend за ~1 секунду зчитає 50 рядків:
  7. notification: Синхронізація: success — Created: 50, replays: 0, failed: 0.
  8. Перевантаж сторінку → у блоці «Поточний стан»:
  9. Курсор: 1050 (event_id останнього рядка CSV).
  10. Останній sync: щойно.
  11. У Sync history блок — рядок з 50 created.
  12. Перевір AttendanceLog:
    venv/Scripts/python.exe -c "
    import django, os
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eswf.settings.development')
    django.setup()
    from hrm.models import AttendanceLog
    print(f'Total: {AttendanceLog.objects.filter(tenant_id=6).count()}')
    print(f'  source=gatehouse: {AttendanceLog.objects.filter(tenant_id=6, source=\"gatehouse\").count()}')
    print(f'  source=fortnet:   {AttendanceLog.objects.filter(tenant_id=6, source=\"fortnet\").count()}')
    "
    
    Очікувано: ~25 з source='fortnet' (5 employees × 5 days з парами IN/OUT).

5.3 Інкрементальний pull

  1. Відкрий CSV вручну, додай новий рядок:
    1051,EMP001,Іванов І.І.,AABBCC01,2,GATE-MAIN,IN,2026-04-26 09:00:00
    
  2. На FortNet page → ще раз Синхронізувати зараз.
  3. Очікувано: Created: 1, replays: 49 (попередні рядки з event_id ≤ 1050 пропущено), failed: 0. Cursor → 1051.

5.4 Recompute days_worked

  1. Відкрий http://localhost:5173/hr-manager/.../payrollSlips/ (HRM).
  2. Знайди slip за квітень для employee EMP001 (Іванов І.І.).
  3. У формі slip → action recompute-days-worked (або через API: POST /api/v1/hrm/payroll-slips/{id}/recompute-days-worked/).
  4. days_worked оновлюється до 5 (5 робочих днів з парами IN/OUT у CSV).

5.5 ContainerHub bridge

  1. Залежить від наявності встановленого containerhub plugin (appType=module, paid).
  2. Якщо встановлено + є Container з контейнером MAEU1234567:
  3. Через webcam-tester або anpr-cli → payload.container_number = 'MAEU1234567'.
  4. Backend ANPR-flow → register_vehicle_gate_eventbridge_to_containerhub_if_installedcontainerhub.GateTransaction.objects.create(container, vehicle, transaction_time, transaction_type, evidence).
  5. Перевір у containerhub:
    venv/Scripts/python.exe -c "
    import django, os
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eswf.settings.development')
    django.setup()
    from containerhub.models import GateTransaction
    for gt in GateTransaction.objects.order_by('-id')[:5]:
        print(gt.id, gt.container_id, gt.vehicle_id, gt.transaction_time, gt.transaction_type)
    "
    

DoD ✅

  • [ ] Прикладали картку → AttendanceLog з source='gatehouse'.
  • [ ] FortNet-sync читає CSV → AttendanceLog з source='fortnet'.
  • [ ] Інкрементальний pull створює лише нові події (cursor рухається).
  • [ ] PayrollSlip.days_worked перераховується з історії.
  • [ ] (Опційно) ContainerHub bridge створює GateTransaction.

Якщо щось зламалося

Симптом Причина / фікс
Card emulator пише, але AttendanceLog не з'являється Bridge на не-personnel gate_point — змінь gate_point_id у toml на 2 (gate_personnel)
FortNet sync 0 created gate_mapping у settings не співпадає з реальним GatePoint.code — seed-default {'GATE-MAIN': 'gate_personnel'}
FortNet sync failed > 0 F12 → last_log.failures показує конкретні рядки. Типово employee_not_found або gate_not_found
recompute-days-worked повертає 0 Перевір AttendanceLog у тому ж period (year+month). Обидва in_at AND out_at потрібні для повного дня
ContainerHub bridge silent Не встановлено plugin (apps.is_installed('containerhub') → False) — так і має бути

Чеклист перед сдачею (прохід усіх 5 sprints)

[ ] Postgres up + migrations applied + seed v5 ran
[ ] Frontend dev (5173) + backend dev (8000) running
[ ] launcher-bridge running with scale + card emulators

S1: WeighingTicket manual flow ✅
[ ] Створив new ticket → Брутто → Тара → Закрити → net правильний

S2: Hardware bridge + WS + EasyOCR ✅
[ ] Live-feed scale events + WS online indicator
[ ] Webcam ANPR ≥1 з 3 спроб

S3: Vehicle gate + QR + checkpoint ✅
[ ] ANPR → Accept → PDF з QR
[ ] QR scan → checkpoint у timeline
[ ] Trace показує open → closed

S4: Weighing correlator + GR ✅
[ ] Correlator pair scale + ANPR
[ ] WeighingTicket weighed_in / closed без ручного вводу
[ ] GR створюється + back-link

S5: Personnel + FortNet ✅
[ ] Own SCUD: card emulator → AttendanceLog gatehouse
[ ] FortNet: 50 rows → 25 AttendanceLog fortnet
[ ] Incremental pull: cursor moves
[ ] recompute_days_worked = 5

[ ] tsc --noEmit clean
[ ] vitest dependencies.test.ts 24/24
[ ] manage.py check 0 issues