From d4f18b886fe0aa0bd8629252d8de926280cb9bd7 Mon Sep 17 00:00:00 2001 From: t0xa Date: Tue, 10 Feb 2026 15:50:37 +0300 Subject: [PATCH] Update some work schemes --- .../puml/Crowd/Tevian/crowd_tevian_new.puml | 92 +++ .../crowd_tevian_now.puml} | 0 .../Tevian/crowd_tevian_now_simplified.puml | 249 ++++++++ .../Crowd/Tevian/crowd_tevian_processing.puml | 59 ++ ivideon/puml/Crowd/cl_analyzer_2.puml | 0 ivideon/puml/Crowd/cl_crowd_entity.puml | 0 ivideon/puml/Crowd/cl_entites_structure.puml | 0 ivideon/puml/Crowd/cl_folder.puml | 540 ------------------ ivideon/puml/Crowd/cl_get_all_cameras | 326 ----------- ivideon/puml/Crowd/entities.puml | 58 -- ivideon/{ => puml}/Gantt/2026_sprints.puml | 0 ivideon/{ => puml}/Gantt/sprint_2.puml | 0 ivideon/{ => puml}/Gantt/sprint_3.puml | 0 ivideon/puml/arch/AN_API_infra.puml | 83 +++ ivideon/puml/arch/AN_folders.puml | 85 +++ 15 files changed, 568 insertions(+), 924 deletions(-) create mode 100644 ivideon/puml/Crowd/Tevian/crowd_tevian_new.puml rename ivideon/puml/Crowd/{cl_analyzer_1.puml => Tevian/crowd_tevian_now.puml} (100%) create mode 100644 ivideon/puml/Crowd/Tevian/crowd_tevian_now_simplified.puml create mode 100644 ivideon/puml/Crowd/Tevian/crowd_tevian_processing.puml delete mode 100644 ivideon/puml/Crowd/cl_analyzer_2.puml delete mode 100644 ivideon/puml/Crowd/cl_crowd_entity.puml delete mode 100644 ivideon/puml/Crowd/cl_entites_structure.puml delete mode 100644 ivideon/puml/Crowd/cl_folder.puml delete mode 100644 ivideon/puml/Crowd/cl_get_all_cameras delete mode 100644 ivideon/puml/Crowd/entities.puml rename ivideon/{ => puml}/Gantt/2026_sprints.puml (100%) rename ivideon/{ => puml}/Gantt/sprint_2.puml (100%) rename ivideon/{ => puml}/Gantt/sprint_3.puml (100%) create mode 100644 ivideon/puml/arch/AN_API_infra.puml create mode 100644 ivideon/puml/arch/AN_folders.puml diff --git a/ivideon/puml/Crowd/Tevian/crowd_tevian_new.puml b/ivideon/puml/Crowd/Tevian/crowd_tevian_new.puml new file mode 100644 index 0000000..2cbe862 --- /dev/null +++ b/ivideon/puml/Crowd/Tevian/crowd_tevian_new.puml @@ -0,0 +1,92 @@ +@startuml Crowd Node - Request Processing Flow + +!define COMPONENT_BG_COLOR #E3F2FD +!define API_BG_COLOR #FFF3E0 +!define STORAGE_BG_COLOR #F3E5F5 + +title Crowd Node: Процесс обработки запроса на анализ изображения + +participant "Scheduler" as Scheduler +participant "Redis Queue" as RedisQueue #FFCCCC +participant "tasks.py" as TaskListener COMPONENT_BG_COLOR +participant "analyzer.py" as Analyzer COMPONENT_BG_COLOR +participant "frames.py" as FramePuller COMPONENT_BG_COLOR +participant "LivePreview service" as LivePreview API_BG_COLOR +box #LightGreen +participant "PersonDetector" as PersonDetector +participant "PersonDetectionService" as PersonDetectionService +end box +participant "Redis Cache" as RedisCache #FFCCCC +participant "S3 Storage" as S3 STORAGE_BG_COLOR +participant "Central" as Central API_BG_COLOR + +== Получение задания == + +Scheduler -> RedisQueue: push task\n{cmd: "analyze_crowd",\nparams: {uin, camera_id, zones, ...}} +activate RedisQueue + +TaskListener -> RedisQueue: pull_task() +activate TaskListener +RedisQueue --> TaskListener: task data +deactivate RedisQueue + +TaskListener -> Analyzer: analyze_crowd(uin, camera_id, zones, **options) +activate Analyzer + +== Получение кадра == + +Analyzer -> FramePuller: pull(uin, camera) +activate FramePuller +FramePuller -> LivePreview: GET /internal/preview +LivePreview --> FramePuller: JPEG image bytes +FramePuller -> FramePuller: Frame(content)\n- создает PIL Image\n- генерирует image_id +FramePuller --> Analyzer: Frame object +deactivate FramePuller + +group #LightGreen Новый подход +== Взаимодействие с новым сервисом== + Analyzer -> PersonDetector: _run_detecotrs(uin, camera, frame, zones) + activate PersonDetector + PersonDetector -> PersonDetector: prepare() - Метод для подготовки\nданных для отправки в сервис + PersonDetector -> PersonDetectionService: POST /picture/analyze\nМетод для анализа + activate PersonDetectionService + PersonDetectionService -> PersonDetectionService: Сервис анализирует изображение + PersonDetectionService --> PersonDetector: {results:\n\t[detection_1, detection_2, ...]\n} + deactivate PersonDetectionService + PersonDetector -> PersonDetector: parse_response() - Метод\nдля приведения резултата в формат,\nкоторый был раньше +end +PersonDetector --> Analyzer: result dict\n{zone_id: {count, objects}, ...} +deactivate PersonDetector + + +== Форматирование результата == +deactivate Detector + +== Сохранение результата == + +Analyzer -> S3: storage.upload_fileobj(\n image, bucket, key, ...) +S3 --> Analyzer: ObjRef + +Analyzer -> S3: storage.generate_presigned_url(obj_ref) +S3 --> Analyzer: presigned_url + +== Отправка результата в Central == + +Analyzer -> Central: central.send('new_measurement', {\n timestamp,\n camera_id,\n measurement_id,\n image: zones_url,\n timings,\n errors,\n zones: zones_info\n}) +note right + Отправка через aio_broker + в очередь 'overmind:input' + с командой 'new_measurement' +end note +Central --> Analyzer: (async, no wait) + +Analyzer -> Analyzer: Обновить БД zones_db:\ndetected_at = time.time() + +Analyzer --> TaskListener: complete +deactivate Analyzer + +TaskListener -> TaskListener: Ожидать следующую задачу +deactivate TaskListener + +@enduml + diff --git a/ivideon/puml/Crowd/cl_analyzer_1.puml b/ivideon/puml/Crowd/Tevian/crowd_tevian_now.puml similarity index 100% rename from ivideon/puml/Crowd/cl_analyzer_1.puml rename to ivideon/puml/Crowd/Tevian/crowd_tevian_now.puml diff --git a/ivideon/puml/Crowd/Tevian/crowd_tevian_now_simplified.puml b/ivideon/puml/Crowd/Tevian/crowd_tevian_now_simplified.puml new file mode 100644 index 0000000..34ace32 --- /dev/null +++ b/ivideon/puml/Crowd/Tevian/crowd_tevian_now_simplified.puml @@ -0,0 +1,249 @@ +@startuml Crowd Node - Request Processing Flow + +!define COMPONENT_BG_COLOR #E3F2FD +!define API_BG_COLOR #FFF3E0 +!define STORAGE_BG_COLOR #F3E5F5 + +title Crowd Node: Процесс обработки запроса на анализ изображения + +participant "Scheduler" as Scheduler +participant "Redis Queue" as RedisQueue #FFCCCC +participant "tasks.py\n(Task Listener)" as TaskListener COMPONENT_BG_COLOR +participant "analyzer.py\n(Main Analyzer)" as Analyzer COMPONENT_BG_COLOR +participant "frames.py\n(Frame Puller)" as FramePuller COMPONENT_BG_COLOR +participant "LivePreview service" as LivePreview API_BG_COLOR +participant "TevianHeadsDetector\n(detectors/tevian.py)" as Detector COMPONENT_BG_COLOR +participant "Redis Cache" as RedisCache #FFCCCC +participant "Tevian Cloud API\n(ext_api/tevian_api.py)" as TevianAPI API_BG_COLOR +participant "S3 Storage" as S3 STORAGE_BG_COLOR +participant "Central (crowd backend)" as Central API_BG_COLOR + +== Получение задания == + +Scheduler -> RedisQueue: push task\n{cmd: "analyze_crowd",\nparams: {uin, camera_id, zones, ...}} +activate RedisQueue + +TaskListener -> RedisQueue: pull_task() +activate TaskListener +RedisQueue --> TaskListener: task data +deactivate RedisQueue + +TaskListener -> Analyzer: analyze_crowd(uin, camera_id, zones, **options) +activate Analyzer + +== Получение кадра == + +Analyzer -> FramePuller: pull(uin, camera) +activate FramePuller +FramePuller -> LivePreview: GET /internal/preview +activate LivePreview +LivePreview --> FramePuller: JPEG image bytes +deactivate LivePreview +FramePuller -> FramePuller: Frame(content)\n- создает PIL Image\n- генерирует image_id +FramePuller --> Analyzer: Frame object +deactivate FramePuller + +== Запуск детектора == + +Analyzer -> Analyzer: _run_detectors(uin, camera, frame, zones) +Analyzer -> Detector: request(uin, camera, frame, zones) +activate Detector + +== Подготовка Tevian (prepare) == + +Detector -> Detector: prepare(cam_name, zones) +note right + Подготовка включает: + 1. Создание/получение камеры + 2. Синхронизацию очередей (зон) + 3. Обновление параметров зон +end note + +Detector -> Detector: _get_or_create_camera(cam_name) + +Detector -> RedisCache: get_camera(cam_name) +activate RedisCache +RedisCache --> Detector: TCamera or None +deactivate RedisCache + +alt Камеры нет в кеше + Detector -> TevianAPI: TCamera.get_all() + activate TevianAPI + TevianAPI -> TevianAPI: _refresh_token() if needed + TevianAPI -> "Tevian Cloud": GET /api/cameras + "Tevian Cloud" --> TevianAPI: список камер [{id, name, ...}] + TevianAPI --> Detector: [TCamera, ...] + deactivate TevianAPI + + Detector -> RedisCache: set_camera(cam) для каждой + + alt Камера все еще не найдена + Detector -> TevianAPI: TCamera.create(cam_name) + activate TevianAPI + TevianAPI -> "Tevian Cloud": POST /api/cameras\n{name, rtsp, frequency_plan_id, ...} + "Tevian Cloud" --> TevianAPI: {id, name, status, ...} + TevianAPI --> Detector: TCamera + deactivate TevianAPI + Detector -> RedisCache: set_camera(cam) + end +end + +== Синхронизация очередей (зон) == + +Detector -> Detector: _get_camera_queues(cam) +Detector -> RedisCache: get_queue(q_id)\nдля каждого queues_ids камеры +RedisCache --> Detector: TQueue objects + +loop Для каждой зоны из запроса + Detector -> Detector: Конвертировать координаты\nв относительные (0..1) + + alt Очередь не найдена + Detector -> TevianAPI: TQueue.create(cam_id, zone_id, polygon, min_head_size) + activate TevianAPI + TevianAPI -> "Tevian Cloud": POST /api/queues\n{name, camera_id, roi_polygon_relative, ...} + "Tevian Cloud" --> TevianAPI: {id, name, camera_id, ...} + TevianAPI --> Detector: TQueue + deactivate TevianAPI + Detector -> RedisCache: set_queue(queue) + else Параметры зоны изменились + Detector -> TevianAPI: queue.save() + activate TevianAPI + TevianAPI -> "Tevian Cloud": POST /api/queues/{id}\n{roi_polygon_relative, ...} + "Tevian Cloud" --> TevianAPI: updated queue + TevianAPI --> Detector: success + deactivate TevianAPI + Detector -> RedisCache: set_queue(queue) + end +end + +loop Для старых очередей (не в списке зон) + Detector -> TevianAPI: TQueue.delete_by_id(queue_id) + activate TevianAPI + TevianAPI -> "Tevian Cloud": DELETE /api/queues/{id} + "Tevian Cloud" --> TevianAPI: success + TevianAPI --> Detector: success + deactivate TevianAPI + Detector -> RedisCache: delete_queue(queue_id) +end + +Detector -> TevianAPI: cam.refresh() +note right + Обновляем состояние камеры + после изменения очередей +end note +activate TevianAPI +TevianAPI -> "Tevian Cloud": GET /api/cameras/{id} +"Tevian Cloud" --> TevianAPI: {status, is_accepting_snapshots, ...} +TevianAPI --> Detector: updated TCamera +deactivate TevianAPI + +Detector -> RedisCache: set_camera(cam) + +== Отправка снапшота и получение результатов == + +Detector -> Detector: Проверка rate limiting\n(FORCED_WAIT_PERIOD) +note right + Избегаем HTTP 429: Too Many Requests + Ждем если запрос слишком частый +end note + +alt Слишком частые запросы + Detector -> Detector: asyncio.sleep(wait_for) +end + +Detector -> TevianAPI: cam.send_snapshot(frame.data) +activate TevianAPI +TevianAPI -> "Tevian Cloud": POST /api/cameras/{id}/snapshots\nContent-Type: image/jpeg\nbody: +"Tevian Cloud" --> TevianAPI: {snapshot_accepted_at: } +TevianAPI --> Detector: timestamp +deactivate TevianAPI + +Detector -> Detector: asyncio.sleep(TEVIAN_RECOGNITION_DELAY)\n(рекомендуется 12 сек) + +Detector -> TevianAPI: TRecognition.get_many(queues_ids, timestamp) +activate TevianAPI + +loop Polling до получения результатов или timeout + TevianAPI -> "Tevian Cloud": GET /api/recognitions?\nqueues_ids={ids}&utc_timestamp={ts} + "Tevian Cloud" --> TevianAPI: [recognitions...] + + alt Результатов меньше чем очередей + TevianAPI -> TevianAPI: await gen.sleep(2)\nи повторить + else Все результаты получены + TevianAPI -> TevianAPI: break + end +end + +TevianAPI -> TevianAPI: Фильтровать detections:\nоставить только\nfiltered_status == 'passed_filters' +TevianAPI --> Detector: [TRecognition, ...] +deactivate TevianAPI + +== Форматирование результата == + +loop Для каждого recognition + Detector -> RedisCache: get_queue(rec.queue_id) + RedisCache --> Detector: TQueue + + Detector -> Detector: Форматировать objects:\n[{x, y, w, h}, ...]\nиз bbox данных + + Detector -> Detector: result[queue.name] = {\n 'count': len(objects),\n 'objects': objects\n} +end + +Detector --> Analyzer: result dict\n{zone_id: {count, objects}, ...} +deactivate Detector + +Analyzer -> Analyzer: _build_zones_info(zones, detected_values) +note right + Объединяет данные зон с результатами + детекторов, определяет length_by_ai +end note + +Analyzer -> Analyzer: _get_triggered_zones(zones_info, timestamp) +note right + Определяет зоны для подсветки + на основе trigger_at и trigger_type +end note + +== Сохранение результата == + +Analyzer -> Analyzer: frame.draw_zones(triggered_zones) +note right + Рисует полигоны триггерных зон + на изображении с прозрачностью +end note + +Analyzer -> Analyzer: resize_image(image, 640) + +Analyzer -> S3: storage.upload_fileobj(\n image, bucket, key, ...) +activate S3 +S3 --> Analyzer: ObjRef +deactivate S3 + +Analyzer -> S3: storage.generate_presigned_url(obj_ref) +activate S3 +S3 --> Analyzer: presigned_url +deactivate S3 + +Analyzer -> Analyzer: Удалить query params\nиз URL (сделать публичным) + +== Отправка результата в Central == + +Analyzer -> Central: central.send('new_measurement', {\n timestamp,\n camera_id,\n measurement_id,\n image: zones_url,\n timings,\n errors,\n zones: zones_info\n}) +activate Central +note right + Отправка через aio_broker + в очередь 'overmind:input' + с командой 'new_measurement' +end note +Central --> Analyzer: (async, no wait) +deactivate Central + +Analyzer -> Analyzer: Обновить БД zones_db:\ndetected_at = time.time() + +Analyzer --> TaskListener: complete +deactivate Analyzer + +TaskListener -> TaskListener: Ожидать следующую задачу +deactivate TaskListener + +@enduml diff --git a/ivideon/puml/Crowd/Tevian/crowd_tevian_processing.puml b/ivideon/puml/Crowd/Tevian/crowd_tevian_processing.puml new file mode 100644 index 0000000..7bb79fd --- /dev/null +++ b/ivideon/puml/Crowd/Tevian/crowd_tevian_processing.puml @@ -0,0 +1,59 @@ +@startuml Crowd Node - Request Processing Flow + +!define COMPONENT_BG_COLOR #E3F2FD +!define API_BG_COLOR #FFF3E0 +!define STORAGE_BG_COLOR #F3E5F5 + +participant "tasks.py" as TaskListener COMPONENT_BG_COLOR +participant "analyzer.py" as Analyzer COMPONENT_BG_COLOR +participant "PersonDetector" as PersonDetector +participant "PersonDetectionService" as PersonDetectionService +participant "Redis Queue" as RedisQueue #FFCCCC +participant "S3 Storage" as S3 STORAGE_BG_COLOR +participant "Central" as Central API_BG_COLOR + +== Вариант 1: Получаем задачу в analyze, процессим синхронно и отдаем ответ == +TaskListener -> Analyzer : Получена задача на процессинг +Analyzer -> PersonDetector : Отправка задачи на анализ +PersonDetector -> PersonDetectionService : Установка HTTP соединения +group Открытое HTTP соединение + PersonDetector -> PersonDetectionService : HTTP POST запрос + PersonDetectionService -> PersonDetectionService : Обарботка запроса + PersonDetectionService --> PersonDetector : Отправка в response результата +end group +PersonDetector --> Analyzer : Результаты задачи +== Вариант 2: Получаем задачу в analyze, кладем в очередь, процессим в очереди, формируем результат и отдаем в ответе analyze == +TaskListener -> Analyzer : Получена задача на процессинг +Analyzer -> RedisQueue : Положили в очередь задачу на анализ изображения +group Polling + PersonDetector -> RedisQueue : полит очередь на предмет наличия задач + RedisQueue --> PersonDetector : Получает задачу на анализ + group Асинхронный запрос на анализ + PersonDetector -> PersonDetectionService : Дерганье API сервиса для анализа + PersonDetectionService -> PersonDetectionService : Обарботка запроса + PersonDetectionService --> PersonDetector : Отправка в response результата + end group +end group + +group Polling + Analyzer -> RedisQueue : Полит в ожидании выполненных задач + RedisQueue --> Analyzer : Выполненные задачи анализа +end group +== Вариант 3: Получаем задачу в analyze, процессим синхронно, формируем результат, отдаем в ответе get_results == +note over PersonDetector + Такое ощущение что это похоже не вариант 1 +end note +== Вариант 4: Получаем задачу в analyze кладем в очередь, процессим в очереди, формируем результат и отдаем в ответе get_results == +TaskListener -> Analyzer : Получена задача на процессинг +activate Analyzer +Analyzer -> PersonDetector : Отправка задачи на анализ\nво внутреннюю очередь PersonDetector'a +group Внутрянка PersonDetector'a + PersonDetector -> PersonDetector: как то хэндлит запросы на обработку + group Обработка в порядке очереди + PersonDetector -> PersonDetectionService : отправляет HTTP запросы на обработку\nпо внутренней логике + PersonDetectionService --> PersonDetector + PersonDetector --> Analyzer : Результат задачи на обработку + deactivate Analyzer + end group +end group +@enduml \ No newline at end of file diff --git a/ivideon/puml/Crowd/cl_analyzer_2.puml b/ivideon/puml/Crowd/cl_analyzer_2.puml deleted file mode 100644 index e69de29..0000000 diff --git a/ivideon/puml/Crowd/cl_crowd_entity.puml b/ivideon/puml/Crowd/cl_crowd_entity.puml deleted file mode 100644 index e69de29..0000000 diff --git a/ivideon/puml/Crowd/cl_entites_structure.puml b/ivideon/puml/Crowd/cl_entites_structure.puml deleted file mode 100644 index e69de29..0000000 diff --git a/ivideon/puml/Crowd/cl_folder.puml b/ivideon/puml/Crowd/cl_folder.puml deleted file mode 100644 index 34f6dab..0000000 --- a/ivideon/puml/Crowd/cl_folder.puml +++ /dev/null @@ -1,540 +0,0 @@ -@startuml Folder System Architecture - - title Система папок/групп в Ivideon - - package "Database" @startuml FolderSystemSimple - - title Система папок в Ivideon - - entity "folders" as folders_db { - * _id : ObjectId - -- - * owner_id : string - * name : string - * parents : array - * objects : array - * root : boolean - } - - entity "permission_grants" as grants_db { - * _id : ObjectId - -- - * object_id : string - * object_type : string - * grantee_id : string - * permissions : array - } - - entity "servers" as servers_db { - * _id : ObjectId - -- - * owner_id : string - * cameras : object - } - - entity "Folder" as folder_class { - + get_objects(type) - + add_object(obj) - + remove_object(obj) - + has_permissions(perm) - } - - entity "FolderTree" as tree_class { - + folders : dict - + find_folders() - + reload() - } - - entity "Camera" as camera_node { - + id : "server:index" - + object_type : "camera" - } - - folders_db ||--o{ folder_class - grants_db ||--o{ folder_class - servers_db ||--o{ camera_node - - tree_class --> folder_class : manages - folder_class --> camera_node : contains - - note right of folders_db - objects[] format: - [ - {object_type: "camera", - object_id: "server:0"}, - {object_type: "folder", - object_id: "subfolder_id"} - ] - end note - - note bottom of tree_class - Usage: - tree = FolderTree(user_id) - folder = tree.folders[folder_id] - cameras = folder.get_objects("camera") - end note - - @enduml -@startuml FolderSystemSimple - - title Система папок в Ivideon - - entity "folders" as folders_db { - * _id : ObjectId - -- - * owner_id : string - * name : string - * parents : array - * objects : array - * root : boolean - } - - entity "permission_grants" as grants_db { - * _id : ObjectId - -- - * object_id : string - * object_type : string - * grantee_id : string - * permissions : array - } - - entity "servers" as servers_db { - * _id : ObjectId - -- - * owner_id : string - * cameras : object - } - - entity "Folder" as folder_class { - + get_objects(type) - + add_object(obj) - + remove_object(obj) - + has_permissions(perm) - } - - entity "FolderTree" as tree_class { - + folders : dict - + find_folders() - + reload() - } - - entity "Camera" as camera_node { - + id : "server:index" - + object_type : "camera" - } - - folders_db ||--o{ folder_class - grants_db ||--o{ folder_class - servers_db ||--o{ camera_node - - tree_class --> folder_class : manages - folder_class --> camera_node : contains - - note right of folders_db - objects[] format: - [ - {object_type: "camera", - object_id: "server:0"}, - {object_type: "folder", - object_id: "subfolder_id"} - ] - end note - - note bottom of tree_class - Usage: - tree = FolderTree(user_id) - folder = tree.folders[folder_id] - cameras = folder.get_objects("camera") - end note - - @enduml -@startuml FolderSystemSimple - - title Система папок в Ivideon - - entity "folders" as folders_db { - * _id : ObjectId - -- - * owner_id : string - * name : string - * parents : array - * objects : array - * root : boolean - } - - entity "permission_grants" as grants_db { - * _id : ObjectId - -- - * object_id : string - * object_type : string - * grantee_id : string - * permissions : array - } - - entity "servers" as servers_db { - * _id : ObjectId - -- - * owner_id : string - * cameras : object - } - - entity "Folder" as folder_class { - + get_objects(type) - + add_object(obj) - + remove_object(obj) - + has_permissions(perm) - } - - entity "FolderTree" as tree_class { - + folders : dict - + find_folders() - + reload() - } - - entity "Camera" as camera_node { - + id : "server:index" - + object_type : "camera" - } - - folders_db ||--o{ folder_class - grants_db ||--o{ folder_class - servers_db ||--o{ camera_node - - tree_class --> folder_class : manages - folder_class --> camera_node : contains - - note right of folders_db - objects[] format: - [ - {object_type: "camera", - object_id: "server:0"}, - {object_type: "folder", - object_id: "subfolder_id"} - ] - end note - - note bottom of tree_class - Usage: - tree = FolderTree(user_id) - folder = tree.folders[folder_id] - cameras = folder.get_objects("camera") - end note - - @enduml -@startuml FolderSystemSimple - - title Система папок в Ivideon - - entity "folders" as folders_db { - * _id : ObjectId - -- - * owner_id : string - * name : string - * parents : array - * objects : array - * root : boolean - } - - entity "permission_grants" as grants_db { - * _id : ObjectId - -- - * object_id : string - * object_type : string - * grantee_id : string - * permissions : array - } - - entity "servers" as servers_db { - * _id : ObjectId - -- - * owner_id : string - * cameras : object - } - - entity "Folder" as folder_class { - + get_objects(type) - + add_object(obj) - + remove_object(obj) - + has_permissions(perm) - } - - entity "FolderTree" as tree_class { - + folders : dict - + find_folders() - + reload() - } - - entity "Camera" as camera_node { - + id : "server:index" - + object_type : "camera" - } - - folders_db ||--o{ folder_class - grants_db ||--o{ folder_class - servers_db ||--o{ camera_node - - tree_class --> folder_class : manages - folder_class --> camera_node : contains - - note right of folders_db - objects[] format: - [ - {object_type: "camera", - object_id: "server:0"}, - {object_type: "folder", - object_id: "subfolder_id"} - ] - end note - - note bottom of tree_class - Usage: - tree = FolderTree(user_id) - folder = tree.folders[folder_id] - cameras = folder.get_objects("camera") - end note - - @enduml -@startuml FolderSystemSimple - - title Система папок в Ivideon - - entity "folders" as folders_db { - * _id : ObjectId - -- - * owner_id : string - * name : string - * parents : array - * objects : array - * root : boolean - } - - entity "permission_grants" as grants_db { - * _id : ObjectId - -- - * object_id : string - * object_type : string - * grantee_id : string - * permissions : array - } - - entity "servers" as servers_db { - * _id : ObjectId - -- - * owner_id : string - * cameras : object - } - - entity "Folder" as folder_class { - + get_objects(type) - + add_object(obj) - + remove_object(obj) - + has_permissions(perm) - } - - entity "FolderTree" as tree_class { - + folders : dict - + find_folders() - + reload() - } - - entity "Camera" as camera_node { - + id : "server:index" - + object_type : "camera" - } - - folders_db ||--o{ folder_class - grants_db ||--o{ folder_class - servers_db ||--o{ camera_node - - tree_class --> folder_class : manages - folder_class --> camera_node : contains - - note right of folders_db - objects[] format: - [ - {object_type: "camera", - object_id: "server:0"}, - {object_type: "folder", - object_id: "subfolder_id"} - ] - end note - - note bottom of tree_class - Usage: - tree = FolderTree(user_id) - folder = tree.folders[folder_id] - cameras = folder.get_objects("camera") - end note - - @enduml -@startuml FolderSystemSimple - - title Система папок в Ivideon - - entity "folders" as folders_db { - * _id : ObjectId - -- - * owner_id : string - * name : string - * parents : array - * objects : array - * root : boolean - } - - entity "permission_grants" as grants_db { - * _id : ObjectId - -- - * object_id : string - * object_type : string - * grantee_id : string - * permissions : array - } - - entity "servers" as servers_db { - * _id : ObjectId - -- - * owner_id : string - * cameras : object - } - - entity "Folder" as folder_class { - + get_objects(type) - + add_object(obj) - + remove_object(obj) - + has_permissions(perm) - } - - entity "FolderTree" as tree_class { - + folders : dict - + find_folders() - + reload() - } - - entity "Camera" as camera_node { - + id : "server:index" - + object_type : "camera" - } - - folders_db ||--o{ folder_class - grants_db ||--o{ folder_class - servers_db ||--o{ camera_node - - tree_class --> folder_class : manages - folder_class --> camera_node : contains - - note right of folders_db - objects[] format: - [ - {object_type: "camera", - object_id: "server:0"}, - {object_type: "folder", - object_id: "subfolder_id"} - ] - end note - - note bottom of tree_class - Usage: - tree = FolderTree(user_id) - folder = tree.folders[folder_id] - cameras = folder.get_objects("camera") - end note - - @enduml -{ - database folders_db as "folders collection" - database grants_db as "permission_grants" - database servers_db as "servers collection" - } - - package "Folder Classes" { - class Folder { - +id: ObjectId - +owner_id: string - +name: string - +parents: List[string] - +objects: List[dict] - +root: boolean - -- - +get_objects(type): List[string] - +add_object(obj) - +remove_object(obj) - +has_permissions(perm): boolean - } - - class FolderTree { - +owner_id: string - +folders: Dict[id, Folder] - +objects: Dict[type, Dict[id, Node]] - +roots: List[Folder] - -- - +find_folders(): List[Folder] - +reload() - } - - class BaseNode { - +id: string - +owner_id: string - +grantee_id: string - +grants: Set[PermissionGrant] - -- - +has_permissions(perm): boolean - +permissions: Tuple[string] - } - } - - package "Permission System" { - class PermissionGrant { - +object_id: string - +object_type: string - +grantee_id: string - +permissions: List[string] - +shared_at: dict - } - } - - package "Node Types" { - class Camera { - +id: "server:index" - +object_type: "camera" - } - } - - ' Relationships - Folder --|> BaseNode - FolderTree --> Folder : manages - Folder --> PermissionGrant : has grants - BaseNode --> PermissionGrant : uses - - Folder --> folders_db : stored in - PermissionGrant --> grants_db : stored in - Camera --> servers_db : stored in - - ' Composition relationships - Folder --> Camera : "contains (objects[])" - Folder --> Folder : "contains subfolders" - - note right of Folder - objects[] содержит: - [ - {object_type: "camera", - object_id: "server:0"}, - {object_type: "folder", - object_id: "subfolder_id"} - ] - end note - - note right of FolderTree - Главная точка доступа: - tree = FolderTree(user_id) - folder = tree.folders[folder_id] - cameras = folder.get_objects("camera") - end note - - note left of PermissionGrant - Права доступа: - - admin (изменение) - - read (просмотр) - - Наследование по иерархии - end note - - @enduml diff --git a/ivideon/puml/Crowd/cl_get_all_cameras b/ivideon/puml/Crowd/cl_get_all_cameras deleted file mode 100644 index f38ec86..0000000 --- a/ivideon/puml/Crowd/cl_get_all_cameras +++ /dev/null @@ -1,326 +0,0 @@ -@startuml _get_all_user_cameras Sequence Diagram - -title Последовательность выполнения _get_all_user_cameras - -participant "Caller" as caller -participant "_get_all_user_cameras" as main_func -participant "_get_servers" as get_servers -participant "MongoDB" as mongo -participant "ivideon.servers" as servers_collection - -note over main_func - Входные параметры: - - user_id: int - - requested_cameras: list[str] - (формат: ["server1:0", "server1:1"]) - - service_name: str (например: "crowd") -end note - -caller -> main_func: _get_all_user_cameras(user_id, requested_cameras, service_name) -activate main_func - -main_func -> main_func: cameras = {} - -main_func -> get_servers: _get_servers(requested_cameras) -activate get_servers - -note over get_servers - Извлекает server_ids из camera_ids: - ["server1:0", "server1:1"] - → ["server1", "server1"] - → ["server1"] -end note -@startuml _get_all_user_cameras Activity Diagram - -title Алгоритм работы _get_all_user_cameras - -start - -note right - **Входные параметры:** - • user_id: int - • requested_cameras: list[str] - (формат: ["server1:0", "server1:1"]) - • service_name: str (например: "crowd") -end note - -:Инициализация cameras = {}; - -:Извлечь server_ids из requested_cameras| -note right - ["server1:0", "server1:1"] - → ["server1"] -end note - -:Построить MongoDB запрос: -query = { - 'deleted': {'$ne': True}, - '_id': {'$in': server_ids} -}| - -:Задать проекцию полей: -projection = { - '_id': 1, 'owner_id': 1, 'name': 1, - 'cameras': 1, 'cam_services': 1, - 'info': 1, 'timezone': 1 -}| - -:Выполнить запрос к MongoDB: -servers = db.ivideon().servers.find(query, projection)| - -partition "Обработка серверов" { - :Взять следующий server; - - while (Есть серверы для обработки?) is (да) - :server_id = server['_id']; - :is_shared = server['owner_id'] != user_id; - :server_build_type = server.get('info', {}).get('build_type', ''); - :is_server_embedded = server_build_type.endswith('camera'); - :cam_services = server.get('cam_services', {}); - - partition "Обработка камер сервера" { - :Взять следующую камеру (camera_idx, camera_data); - - while (Есть камеры на сервере?) is (да) - :service_info = cam_services.get(camera_idx, {}) - .get(service_name, {}); - - if (service_info.get('active', False) == True?) then (да) - :camera_id = f'{server_id}:{camera_idx}'; - - if (is_server_embedded?) then (да) - :camera_name = server['name']; - else (нет) - :camera_name = camera_data.get('name'); - endif - - :cameras[camera_id] = { - 'id': camera_id, - 'owner_id': server['owner_id'], - 'server': server_id, - 'name': camera_name, - 'is_shared': is_shared, - 'timezone': server.get('timezone') or - server.get('timezone_default'), - 'is_embedded': is_server_embedded - }; - - else (нет) - note right: Камера пропускается - сервис неактивен - endif - - :Взять следующую камеру (camera_idx, camera_data); - endwhile (нет) - } - - :Взять следующий server; - endwhile (нет) -} - -:return cameras; - -stop - -note left - **Результат:** dict[camera_id, camera_info] - - **Пример:** - { - "507f...439011:0": { - "id": "507f...439011:0", - "owner_id": "user123", - "server": "507f...439011", - "name": "Камера входа", - "is_shared": false, - "timezone": "Europe/Moscow", - "is_embedded": false - } - } -end note - -@enduml -ggVG -get_servers -> get_servers: requested_server_ids = [camera_id.split(':')[0] \\nfor camera_id in requested_camera_ids] - -get_servers -> get_servers: query = {\n 'deleted': {'$ne': True},\n '_id': {'$in': requested_server_ids}\n} - -get_servers -> get_servers: projection = {\n '_id': 1, 'owner_id': 1, 'name': 1,\n 'cameras': 1, 'cam_services': 1,\n 'info': 1, 'timezone': 1\n} - -get_servers -> mongo: db.ivideon().servers.find(query, projection) -activate mongo -mongo -> servers_collection: find documents -activate servers_collection -servers_collection -> mongo: return server documents -deactivate servers_collection -mongo -> get_servers: list[server_documents] -deactivate mongo - -get_servers -> main_func: return servers_list -deactivate get_servers - -loop for each server in servers_list - main_func -> main_func: server@startuml _get_all_user_cameras Activity Diagram - -title Алгоритм работы _get_all_user_cameras - -start - -note right - **Входные параметры:** - • user_id: int - • requested_cameras: list[str] - (формат: ["server1:0", "server1:1"]) - • service_name: str (например: "crowd") -end note - -:Инициализация cameras = {}; - -:Извлечь server_ids из requested_cameras| -note right - ["server1:0", "server1:1"] - → ["server1"] -end note - -:Построить MongoDB запрос: -query = { - 'deleted': {'$ne': True}, - '_id': {'$in': server_ids} -}| - -:Задать проекцию полей: -projection = { - '_id': 1, 'owner_id': 1, 'name': 1, - 'cameras': 1, 'cam_services': 1, - 'info': 1, 'timezone': 1 -}| - -:Выполнить запрос к MongoDB: -servers = db.ivideon().servers.find(query, projection)| - -partition "Обработка серверов" { - :Взять следующий server; - - while (Есть серверы для обработки?) is (да) - :server_id = server['_id']; - :is_shared = server['owner_id'] != user_id; - :server_build_type = server.get('info', {}).get('build_type', ''); - :is_server_embedded = server_build_type.endswith('camera'); - :cam_services = server.get('cam_services', {}); - - partition "Обработка камер сервера" { - :Взять следующую камеру (camera_idx, camera_data); - - while (Есть камеры на сервере?) is (да) - :service_info = cam_services.get(camera_idx, {}) - .get(service_name, {}); - - if (service_info.get('active', False) == True?) then (да) - :camera_id = f'{server_id}:{camera_idx}'; - - if (is_server_embedded?) then (да) - :camera_name = server['name']; - else (нет) - :camera_name = camera_data.get('name'); - endif - - :cameras[camera_id] = { - 'id': camera_id, - 'owner_id': server['owner_id'], - 'server': server_id, - 'name': camera_name, - 'is_shared': is_shared, - 'timezone': server.get('timezone') or - server.get('timezone_default'), - 'is_embedded': is_server_embedded - }; - - else (нет) - note right: Камера пропускается - сервис неактивен - endif - - :Взять следующую камеру (camera_idx, camera_data); - endwhile (нет) - } - - :Взять следующий server; - endwhile (нет) -} - -:return cameras; - -stop - -note left - **Результат:** dict[camera_id, camera_info] - - **Пример:** - { - "507f...439011:0": { - "id": "507f...439011:0", - "owner_id": "user123", - "server": "507f...439011", - "name": "Камера входа", - "is_shared": false, - "timezone": "Europe/Moscow", - "is_embedded": false - } - } -end note - -@enduml -ggVG_id = server['_id'] - main_func -> main_func: is_shared = server['owner_id'] != user_id - main_func -> main_func: server_build_type = server.get('info', {}).get('build_type', '') - main_func -> main_func: is_server_embedded = server_build_type.endswith('camera') - main_func -> main_func: cam_services = server.get('cam_services', {}) - - loop for camera_idx, camera_data in server.cameras.items() - main_func -> main_func: service_info = cam_services.get(camera_idx, {})\\n .get(service_name, {}) - - alt service_info.get('active', False) == True - main_func -> main_func: camera_id = f'{server_id}:{camera_idx}' - - alt is_server_embedded == True - main_func -> main_func: camera_name = server['name'] - else - main_func -> main_func: camera_name = camera_data.get('name') - end - - main_func -> main_func: cameras[camera_id] = {\n 'id': camera_id,\n 'owner_id': server['owner_id'],\n 'server': server_id,\n 'name': camera_name,\n 'is_shared': is_shared,\n 'timezone': server.timezone,\n 'is_embedded': is_server_embedded\n} - - note right - Создается полная информация - о камере для возврата - end note - - else - note right - Камера пропускается: - сервис неактивен - end note - end - end -end - -main_func -> caller: return cameras dict -deactivate main_func - -note over caller - Результат: dict[camera_id, camera_info] - где camera_id = "server_id:camera_index" - - Пример: - { - "507f...439011:0": { - "id": "507f...439011:0", - "owner_id": "user123", - "server": "507f...439011", - "name": "Камера входа", - "is_shared": false, - "timezone": "Europe/Moscow", - "is_embedded": false - } - } -end note - -@enduml diff --git a/ivideon/puml/Crowd/entities.puml b/ivideon/puml/Crowd/entities.puml deleted file mode 100644 index 46c8cff..0000000 --- a/ivideon/puml/Crowd/entities.puml +++ /dev/null @@ -1,58 +0,0 @@ -@startuml -title Верхнеуровневые сущности сервиса Crowd - -card api_concept{ - entity "CrowdReport(APIObject)" as CRA{ - + id str - + owner_id str - + type str - + name str - + status str - + created_at timestamp - + updated_at timestamp - + progress int - + options dict - + create() -> CrowdReport - } -} - -card crowd_service{ - card backend { - } - - card bot_notifier { - } - - card frontend { - card impl { - entity CrowdReport{ - + delete() -> None - + create() -> CrowdReport - } - } - } - - card node { - } - - card protocols{ - } - - card report_builder{ - } - - card utils { - } - -} - -json options_dict { - "cameras": ["cam1", "cam2"], - "folders": ["folder1"], - "zones": ["zone1"] -} - -CrowdReport ..|> CRA -CRA::options -- options_dict - -@enduml \ No newline at end of file diff --git a/ivideon/Gantt/2026_sprints.puml b/ivideon/puml/Gantt/2026_sprints.puml similarity index 100% rename from ivideon/Gantt/2026_sprints.puml rename to ivideon/puml/Gantt/2026_sprints.puml diff --git a/ivideon/Gantt/sprint_2.puml b/ivideon/puml/Gantt/sprint_2.puml similarity index 100% rename from ivideon/Gantt/sprint_2.puml rename to ivideon/puml/Gantt/sprint_2.puml diff --git a/ivideon/Gantt/sprint_3.puml b/ivideon/puml/Gantt/sprint_3.puml similarity index 100% rename from ivideon/Gantt/sprint_3.puml rename to ivideon/puml/Gantt/sprint_3.puml diff --git a/ivideon/puml/arch/AN_API_infra.puml b/ivideon/puml/arch/AN_API_infra.puml new file mode 100644 index 0000000..08b5369 --- /dev/null +++ b/ivideon/puml/arch/AN_API_infra.puml @@ -0,0 +1,83 @@ +@startuml API Infrastructure with HAProxy +skinparam linetype ortho + +title Архитектура развертывания API (с HAProxy) + +actor "Клиент" as Client +cloud "Internet" as Internet + +package "Kubernetes Cluster" { + + package "Ingress Layer" { + component "Nginx Ingress\nController" as NginxIngress #lightblue + note right of NginxIngress + - HTTPS (443) termination + - TLS (Let's Encrypt) + - X-Forwarded-For + - Domains: + * api.ivideon.com + * api.stage-01.stg01-k8s.extcam.com + end note + } + + package "Proxy Layer" { + component "HAProxy\n(haproxy-central)" as HAProxy #lightgreen + note right of HAProxy + - Port 80 (HTTP) + - ACL routing + - Health checks (/status) + - Backend: api4.service.ivideon:80 + end note + } + + package "Service Layer" { + component "api4 Service" as Service #lightyellow + note right of Service + - Kubernetes Service + - Port 80 → 8080 + - Load balancing + - DNS: api4.service.ivideon + end note + } + + package "Application Layer" { + collections "api4 Pods" as Pods + + component "Pod 1" as Pod1 { + component "Tornado\nHTTP Server" as Tornado1 #orange + note bottom of Tornado1 + - Port: 8080 + - xheaders: true + - Workers: 4 + end note + } + + component "Pod 2-N" as PodN { + component "Tornado\nHTTP Server" as TornadoN #orange + } + } +} + +database "MongoDB\n(main)" as MongoDB +database "MongoDB\n(user_registry)" as UserRegistry + +Client --> Internet: HTTPS\nPOST /public/registration +Internet --> NginxIngress: 443 (HTTPS) +NginxIngress --> HAProxy: 80 (HTTP)\n+ X-Forwarded-For +HAProxy --> Service: api4.service.ivideon:80\n(ACL: !has_api5_components) +Service --> Pods: Round-robin LB +Pods --> Pod1: 8080 +Pods --> PodN: 8080 + +Pod1 --> MongoDB: users.insert_one() +Pod1 --> UserRegistry: check duplicate + +note bottom of HAProxy + **HAProxy ACL Routing:** + - use_backend api4 if host_api !has_api5_components + - Health check: GET /status + - server-template api-four-srv 4 + - option redispatch +end note + +@enduml diff --git a/ivideon/puml/arch/AN_folders.puml b/ivideon/puml/arch/AN_folders.puml new file mode 100644 index 0000000..065dc3b --- /dev/null +++ b/ivideon/puml/arch/AN_folders.puml @@ -0,0 +1,85 @@ +@startuml Folder Structure + +package "MongoDB Collection: folders" { + + object "Root Folder" as root { + _id = "root_abc123" + name = "__root__" + parents = [] + objects = [] + owner_id = "123456" + owner_name = "user@example.com" + root = true + } + + object "Folder: Office" as office { + _id = "folder_office_xyz" + name = "Office" + parents = ["root_abc123"] + objects = [ + {object_type: "camera", object_id: "cam_1"}, + {object_type: "camera", object_id: "cam_2"} + ] + owner_id = "123456" + } + + object "Folder: Warehouse" as warehouse { + _id = "folder_warehouse_qwe" + name = "Warehouse" + parents = ["root_abc123"] + objects = [ + {object_type: "camera", object_id: "cam_3"}, + {object_type: "server", object_id: "srv_1"} + ] + owner_id = "123456" + } + + object "Subfolder: Entrance" as entrance { + _id = "folder_entrance_asd" + name = "Entrance" + parents = ["root_abc123", "folder_office_xyz"] + objects = [ + {object_type: "camera", object_id: "cam_4"} + ] + owner_id = "123456" + } +} + +object "User" as user { + _id = 123456 + login = "user@example.com" + root_folder = "root_abc123" +} + +user --> root : root_folder +root --> office : subfolder +root --> warehouse : subfolder +office --> entrance : subfolder + +note right of root + При создании пользователя + создается пустая root_folder + + objects = [] + parents = [] + root = true +end note + +note right of office + Пользователь может создавать + папки для организации камер: + - Офис + - Склад + - Парковка + и т.д. +end note + +note right of entrance + Поддерживается вложенность: + parents = [root, office] + + Уровень вложенности: + level = len(parents) = 2 +end note + +@enduml