B17/E5: ингестия полевых данных КОНТУРа

- POST /operations/{id}/field-data/sync: тянет GET /api/v1/vector/areas-checked
  → идемпотентный upsert в areas_checked (dedupe по contour_area_id:
  geom/checked_at/result/coverage_pct/team_label, source=contour)
- GET /operations/{id}/areas-checked: теперь из локальной БД после sync
  (source=db), фолбэк на живой прокси КОНТУРа если пусто (source=contour/empty)
- миграция 011_b17_field_data: areas_checked +contour_area_id/team_label/source
  (+check ck_areas_checked_source)
- аудит field_data_sync; лог vector.contour field_sync_*
- фронт: кнопка «⟳ Синхронизировать полевые данные» в КОНТУР-панели,
  слой проверенных квадратов на карте (зелёный=clear ≥99.5%, жёлтый=partial)
- тесты: +9 (267 passed) — sync/идемпотентность/502/аудит/локальное чтение
This commit is contained in:
2026-09-25 10:34:53 +03:00
parent 1fdcc04af6
commit 50a261e05e
7 changed files with 497 additions and 8 deletions
+6 -1
View File
@@ -203,10 +203,15 @@ class AreaChecked(Base):
checked_at = Column(DateTime(timezone=True))
result = Column(String(20), default="partial")
coverage_pct = Column(Float)
created_at = Column(DateTime, server_default=func.now())
created_at = Column(DateTime(timezone=True), server_default=func.now())
# B17: идемпотентный ингест из КОНТУРа
contour_area_id = Column(String(64), index=True) # area_id на стороне КОНТУРа
team_label = Column(String(100)) # callsign цепи (КОНТУР)
source = Column(String(20), default="manual") # manual|contour
__table_args__ = (
sa.CheckConstraint("result IN ('clear','found','partial')", name="ck_areas_checked_result"),
sa.CheckConstraint("source IN ('manual','contour')", name="ck_areas_checked_source"),
)