Files
ui/_docs/legacy/wpf-era.md
Oleksandr Bezdieniezhnykh 510df68bcf [AZ-447] autodev Steps 1-4 baseline: docs, tests, refactor specs
Captures the full output of autodev existing-code Phase A through
Step 4 (Code Testability Revision) for the Azaion UI workspace:

- Step 1 Document: _docs/02_document/ (FINAL_report, architecture,
  glossary, components/, modules/, diagrams/, system-flows,
  module-layout) plus _docs/00_problem/ + _docs/01_solution/ +
  _docs/legacy/ + _docs/how_to_test + README.
- Step 2 Architecture Baseline: architecture_compliance_baseline.md.
- Step 3 Test Spec: _docs/02_document/tests/ (environment,
  test-data, blackbox/performance/resilience/security/
  resource-limit tests, traceability-matrix), enum_spec_snapshot,
  expected_results/results_report.md (98 rows), plus the
  run-tests.sh + run-performance-tests.sh runners.
- Step 4 Code Testability Revision: 01-testability-refactoring/
  run dir (list-of-changes C01-C07, deferred_to_refactor,
  analysis/research_findings + refactoring_roadmap) and the 7
  child task specs AZ-448..AZ-454 under _docs/02_tasks/todo/
  plus _dependencies_table.md.
- _docs/_autodev_state.md pins the cursor at Step 4 / refactor
  Phase 4 entry so /autodev resumes cleanly.

Epic AZ-447 (UI testability gates) tracks the 7 child tasks that
will land in subsequent commits.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 00:38:49 +03:00

25 KiB
Raw Permalink Blame History

Legacy: WPF Era of Azaion (annotations predecessor)

Source of truth for this doc: suite/annotations-research/ — a clone of suite/annotations checked out at commit 22529c2 "Revert add MediaFile" (Mon Nov 17 2025), which is the LAST commit before the big refactoring (e7ea5a8) that started decoupling the WPF UI from the backend in preparation for the WPF→.NET API conversion (9e7dc29 / fbbe556 refactor .net project to API).

This document captures the system as it existed when the annotation tool, the inference engine, the loader, and the UI were a single Windows desktop application with .NET WPF in front and Cython sidecar processes behind it, all running on one machine.

The current azaion/ui repo is the React rewrite of the front-end half of that legacy stack. The Cython parts and the .NET service code became separate suite submodules (detections/, loader/, annotations/ (now .NET API only), flights/, etc.). This file exists so future maintainers can understand where features in the React UI come from, why some shapes in the data look the way they do, and what is intentionally NOT being ported.


1. Top-level layout (Azaion.Suite.sln)

The legacy repository was a single Visual Studio solution containing eight .NET projects plus two Cython projects:

# Project Type Role
1 Azaion.Suite .NET 8 WPF (exe) Application host. DI container, config, module registry, key handler.
2 Azaion.LoaderUI .NET 8 WPF (exe) Login screen. Launches Azaion.Suite.exe with encrypted creds.
3 Azaion.Annotator .NET 8 WPF (lib) Main annotation window: video/image canvas, bounding boxes, AI detect.
4 Azaion.Dataset .NET 8 WPF (lib) Dataset Explorer window: thumbnail grid, class distribution, validation.
5 Azaion.Common .NET 8 (lib) Tangled core: WPF user controls + LinqToDB models + RabbitMQ + HTTP + DTOs all in one assembly.
6 Azaion.CommonSecurity .NET 8 (lib) AES helpers. Credentials persisted to disk encrypted.
7 Azaion.Test .NET 8 test project Unit tests for utilities (intervals, throttle, parallel, tile processing).
8 Dummy placeholder dir empty.
C1 Azaion.Inference Cython (Python) YOLO inference (ONNX / TensorRT). Separate process, ZeroMQ link to .NET.
C2 Azaion.Loader Cython (Python) Encrypted resource fetcher + hardware fingerprinting. Separate process, ZeroMQ link to .NET.

There was no internet-facing API. Everything ran on one Windows machine (operator laptop / OrangePi / Jetson).

+----------------------------- one Windows host -----------------------------+
|                                                                            |
|  Azaion.LoaderUI.exe                                                       |
|     |                                                                      |
|     |  encrypts creds  ->  spawns Azaion.Suite.exe -c <encrypted>          |
|     v                                                                      |
|  Azaion.Suite.exe (WPF)                                                    |
|     |                                                                      |
|     |  Microsoft.Extensions.Hosting + DI                                   |
|     |     - registers Azaion.Annotator, Azaion.Dataset windows             |
|     |     - registers Annotation/Gallery/Inference/GpsMatcher Services     |
|     |     - registers AzaionApi (HttpClient -> remote installer/aux APIs)  |
|     |     - registers LoaderClient + InferenceClient + GpsMatcherClient    |
|     |       (all are ZeroMQ DealerSocket clients)                          |
|     |                                                                      |
|     |--- LinqToDB --------------> SQLite file (annotations.db)             |
|     |--- ZeroMQ Dealer ---------> Azaion.Loader.exe (Cython)               |
|     |                                |                                     |
|     |                                +---> downloads encrypted resources   |
|     |                                      from remote API                 |
|     |                                                                      |
|     |--- ZeroMQ Dealer ---------> Azaion.Inference.exe (Cython)            |
|     |                                |                                     |
|     |                                +---> ONNX / TensorRT inference       |
|     |                                                                      |
|     +--- RabbitMQ.Stream client -> remote RabbitMQ (annotation sync)       |
|                                                                            |
+----------------------------------------------------------------------------+

2. Boot sequence

  1. User runs Azaion.LoaderUI.exe (the launcher / login window).
  2. Login.LoginClick → calls IAzaionApi.Login (HTTP) for installer-version check, then spawns the external Azaion.Loader Cython process and talks to it over ZeroMQ (tcp://127.0.0.1:<port>):
    • CommandType.Login → loader stores credentials and a hardware-derived key (hardware_service.pyx).
    • CommandType.CheckResource → loader verifies it can decrypt the cached encrypted resource bundle.
  3. Login AES-encrypts ApiCredentials (Azaion.CommonSecurity) and starts Azaion.Suite.exe -c <encrypted> then closes itself.
  4. Azaion.Suite.App.Start(creds):
    • Builds a Serilog logger.
    • Builds IConfiguration from three JSON streams: a local config.json, plus config.system.json and config.secured.json fetched from disk via LoaderClient.LoadFile(...) (the Cython loader decrypts them on the fly).
    • Configures the DI container (Microsoft.Extensions.Hosting):
      • IConfigUpdater, Annotator, DatasetExplorer, HelpWindow, MainSuite
      • IDbFactory, IAnnotationService, FailsafeAnnotationsProducer, IGalleryService
      • IInferenceClient/IInferenceService (ZMQ → Cython inference)
      • IGpsMatcherClient/IGpsMatcherService (ZMQ → GPS matcher service)
      • ISatelliteDownloader
      • IAzaionApi (HTTP client to remote API for installer + assets)
      • IAzaionModule registrations (AnnotatorModule, DatasetExplorerModule)
      • MediatR with assemblies from Annotator, DatasetExplorer, Common.
    • Calls Annotation.Init(directoriesConfig, detectionClassesDict) — populates static state on the Annotation entity so that LinqToDB hydrated rows know how to compute ImagePath / LabelPath / ThumbPath / Colors / ClassName. (This static coupling is exactly what the e7ea5a8 "big refactoring" set out to remove.)
    • Hooks a global preview-key handler that publishes a MediatR KeyEvent (with throttle) for any keyboard input that is not in a TextBox.
    • Shows MainSuite (the module switcher window).

3. Module system

Azaion.Suite.MainSuite is a small chrome window with a left-hand ListView of modules. Each module implements:

public interface IAzaionModule
{
    string Name { get; }              // localized display name
    string SvgIcon { get; }           // inline SVG markup
    Type MainWindowType { get; }      // WPF Window subclass
    WindowEnum WindowEnum { get; }    // identifier
}

Two implementations existed at this commit:

  • AnnotatorModuleAzaion.Annotator.Annotator
  • DatasetExplorerModuleAzaion.Dataset.DatasetExplorer

MainSuite shows the icon, opens the corresponding Window from the DI container, and tracks open windows in a Dictionary<WindowEnum, Window> so clicking the same module twice activates instead of recreating.

The IAzaionModule extension point is the seed of what became, in the post-refactor world, the left-hand top-level navigation in the React SPA: Flights, Annotations, Dataset, Admin, Settings.

4. The Annotator window (Azaion.Annotator.Annotator)

The annotation surface (Annotator.xaml.cs, ~600 lines) is the heaviest part of the legacy app. It owned, all in one window:

  • Video/image playback: LibVLCSharp MediaPlayer for video, image decoding for stills.
  • Canvas editor: a custom WPF Canvas with CanvasEditor from Azaion.Common.Controls for click-and-drag bounding boxes, 8-handle resize, multi-select with Ctrl, zoom with Ctrl+wheel, pan with Ctrl+drag, crosshair cursor with active-class hint.
  • Time-windowed annotation overlay during video playback: an IntervalTree<TimeSpan, Annotation> keyed by [Time - 50ms, Time + 150ms]; on each VLC position update, all overlapping intervals render and the rest clear.
  • Detection class strip (Azaion.Common.Controls.DetectionClasses): data grid of class colour + number + name, with PhotoMode switcher (Regular=0, Winter=20, Night=40); class number pressed via keyboard (19); class colour mixed into the bounding-box label.
  • Annotation list (right sidebar): DataGrid over the in-process IntervalTree, gradient-coloured by detection class, double-click seeks the video and zooms.
  • Frame-by-frame controls: 1, 5, 10, 30, 60-frame stepping computed from the video's FPS; play/pause/stop; mute and volume.
  • AI Detect (R key or button): spawns the Cython inference process via IInferenceClient (ZMQ) and streams progress into a modal AutodetectDialog.
  • Camera config side panel (Azaion.Common.Controls.CameraConfigControl): altitude / focal length / sensor width — used to compute GSD-based bounds for valid detection sizes.
  • GPS panel (Azaion.Annotator.Controls.MapMatcher): toggleable below the canvas; ties into IGpsMatcherClient which talks to a separate GPS-denied positioning Cython process.
  • Help window (HelpWindow.xaml + HelpTexts.cs): annotation quality guidelines.
  • Ukrainian / English localisation via translations.json.

Everything in this list is owned by the same Annotator partial class. There is no view-model boundary; XAML code-behind directly:

  • queries IDbFactory for AnnotationsDb, then runs LinqToDB queries against the Annotation, Detection, MediaFile, AnnotationQueueRecord tables;
  • mutates the canvas, the data grid, the VLC media player, and the GPS panel;
  • publishes MediatR notifications (AnnotationCreatedEvent, AnnotationsDeletedEvent, KeyEvent, SetStatusTextEvent, AnnotatorControlEvent, LoadErrorEvent) which downstream services like AnnotationService.OnAnnotationCreated react to (e.g. to enqueue a sync message into the local SQLite buffer table for later RabbitMQ publish).

This is the central tangle. The same class talks to the database, the network (RabbitMQ via mediator), the inference process, the file system, and the WPF visual tree.

5. The Dataset Explorer window (Azaion.Dataset.DatasetExplorer)

Mirrors the annotator, but for browsing:

  • Thumbnail grid (virtualised) keyed by Annotation.ThumbPath, regenerated by IGalleryService.
  • Filter bar: date range, flight, status (AnnotationStatus: None / Created / Edited / Validated).
  • Class distribution chart (Controls/ClassDistribution.xaml): horizontal bars, one per DetectionClass, coloured with the class colour.
  • Inline editor tab — same CanvasEditor from Azaion.Common.Controls, reused.
  • Bulk validation: select multiple thumbnails, press V, status becomes Validated.
  • Local keyboard handlers: 19 (class), Enter (save), Del (delete selected), X (delete all), V (validate), arrow keys + PageUp/PageDown for navigation, Esc to close the editor.

6. Azaion.Common: the everything-bag

This is the assembly that the post-refactor split tries hardest to undo. At commit 22529c2 it was a single .NET project containing:

Folder Concern
Controls/ WPF user controls: CanvasEditor, NumericUpDown, DetectionClasses, CameraConfigControl, DetectionLabelPanel, UpdatableProgressBar, DetectionControl.
Database/ LinqToDB models + AnnotationsDb : DataConnection + DbFactory + SchemaMigrator + AnnotationsDbSchemaHolder.
DTO/ App config sections (AppConfig, LoaderClientConfig, InferenceClientConfig, GpsDeniedConfig, MapConfig, QueueConfig, AIRecognitionConfig, ThumbnailConfig, UIConfig, AnnotationConfig, CameraConfig, DirectoriesConfig), domain enums (AffiliationEnum, RoleEnum, WindowEnum, Direction, PlaybackControlEnum), and shared shapes (ApiCredentials, BusinessExceptionDto, LoginResponse, RemoteCommand, User, DetectionClass, LabelInfo, AnnotationResult, AnnotationThumbnail, ClusterDistribution, Coordinates, SatTile, DownloadTilesResult, SelectionState, FormState, ExternalClientsConfig).
Events/ MediatR notifications (AnnotationCreatedEvent, AnnotationsDeletedEvent, KeyEvent, SetStatusTextEvent, AnnotatorControlEvent, LoadErrorEvent).
Exceptions/ BusinessException.
Extensions/ Helpers — Geo, ParallelExt, ResilienceExt, ThrottleExtensions, IntervalTree-related, Bitmap, Color, Cancellation, Queryable, Graphics, Size, String, DirectoryInfo, DenseDateTimeConverter, EnumExtensions, ServiceCollectionExtensions.
Services/ AnnotationService (RabbitMQ.Stream + LinqToDB + MediatR), FailsafeAnnotationsProducer, GalleryService (thumbnails), AuthProvider, TileProcessor, SatelliteDownloader, LoaderClient (ZMQ), GpsMatcher/* (ZMQ + service + event handler + events), Inference/InferenceClient (ZMQ), Inference/InferenceService (orchestrates inference jobs), Inference/InferenceServiceEventHandler, Inference/InferenceServiceEvents, Cache, HashExtensions.
Constants.cs CONFIG_PATH, suffixes, file naming conventions, the FailsafeAppConfig builder.
Security.cs AES-256-CFB credentials encryption / decryption. Key is time-derived for local on-disk storage; symmetrical on both ends of the loader handoff.

Concrete examples of the tangle, taken straight from this commit:

  • Azaion.Common.Database.Annotation carries [IgnoreMember] System.Windows.Media.Color in its computed Colors projection. A "database model" that imports System.Windows.Media. The DTO assembly cannot exist outside WPF.
  • Annotation.Init(DirectoriesConfig, Dictionary<int, DetectionClass>) is a static initializer that the application calls once at startup. Hydrated entities then read the static _labelsDir, _imagesDir, _thumbDir, DetectionClassesDict to compute their own paths and class names. Two annotation databases or two configurations cannot coexist in the same process.
  • AppConfig aggregates ten config sections including UIConfig, but is also passed to the queue producer, the loader client, the inference client, the satellite downloader. There is no clear seam between "app-host concerns" and "business concerns".
  • AnnotationService is constructed with IDbFactory, FailsafeAnnotationsProducer, QueueConfig, UIConfig, IGalleryService, IMediator, IAzaionApi, ILogger. It runs a RabbitMQ.Stream.Consumer inside its constructor via Task.Run(...).Wait(), publishes MediatR events into the WPF dispatcher, and uses _imageAccessSemaphore and _messageProcessingSemaphore to serialize cross-thread SQLite writes. Lifecycle and threading model are baked in.

The follow-on commits (e7ea5a8, 9e7dc29, fbbe556) split this into proper layers: repositories with interfaces, an AnnotationPathResolver service replacing the static fields on Annotation, a separate Azaion.Common.Database.AnnotationRepository + IAnnotationRepository, removal of AnnotationsDbSchemaHolder, and finally the move from a WPF client to a containerised .NET API exposing REST + SSE.

7. Cython sidecars

Azaion.Inference

Standalone Python project compiled with Cython (build_inference.cmd → PyInstaller → azaion-inference.spec). Top-level modules at 22529c2:

ai_availability_status   ai_config           annotation
classes.json             constants_inf       file_data
inference                inference_engine    onnx_engine
loader_client            main_inference      remote_command_handler_inf

It exposes a ZeroMQ DealerSocket port. The .NET side (InferenceClient in Azaion.Common.Services.Inference) sends MessagePack-serialised RemoteCommand envelopes; the Cython side dispatches to either the ONNX or TensorRT engine, reads inputs from the local file system, and streams back DetectionEvent-shaped progress.

State the engine reports (mapped 1:1 to the React UI's AIAvailabilityStatus):

Value Name Meaning
0 None Initial.
10 Downloading Pulling weights from Admin API / CDN via loader_client.
20 Converting ONNX → TensorRT (TensorRT devices only).
30 Uploading Uploading converted engine back to API for caching.
200 Enabled Inference engine ready.
300 Warning Recoverable, the engine may come back.
500 Error Failed to initialize.

Azaion.Loader

Same shape — Cython, ZeroMQ DealerSocket, separate process. Modules:

api_client            cdn_manager        constants
credentials           file_data          hardware_service
main_loader           remote_command     remote_command_handler
security

It is the only component in the legacy stack with internet access. It authenticates the user against the remote API, downloads encrypted resource bundles (model checkpoints, config.system.json, config.secured.json), and decrypts them on demand using a key derived from email + password + hardware_id (security.pyx + hardware_service.pyx). The .NET side never sees the raw resource files until the loader has already decrypted them.

This Loader is exactly the component documented in suite/_docs/00_top_level_architecture.md under Binary Split Security. The 3 KB key fragment, the encrypted on-device archive, and the SHA384(fragment + hw_hash + creds) derivation all originate here.

8. Data model (LinqToDB → SQLite)

Table Purpose
Annotations Per-frame label set: Name, MediaHash, OriginalMediaName, Time, CreatedDate, CreatedEmail, CreatedRole, Source, AnnotationStatus, ValidateDate, ValidateEmail, Detections[], Milliseconds, Lat, Lon.
Detections Bounding box rows: ClassNumber, geometry, Confidence.
MediaFiles Files indexed by hash. Used to dedupe + drive the media list.
AnnotationQueueRecord (AnnotationsQueueRecords table) Local failsafe outbox for RabbitMQ publication. FailsafeAnnotationsProducer drains this every 10 s.

Schema is created/migrated in process by SchemaMigrator against a SQLite file pointed to by DirectoriesConfig.

9. Annotation sync (edge → central)

The legacy code already had the eventual edge-to-central sync wired in:

Annotator window          // user creates annotation
   │ MediatR: AnnotationCreatedEvent
   ▼
AnnotationService         // local SQLite write
   │
   ├─► Annotations row
   ├─► AnnotationQueueRecord row (unless SilentDetection)
   │
   ▼
FailsafeAnnotationsProducer // BackgroundService-style task
   │ MessagePack + Gzip, retry on failure
   ▼
RabbitMQ.Stream "azaion-annotations"  // remote
   │
   └─► consumed by ai-queue-handler / Admin API in the remote tier

The React UI inherits the protocol (RabbitMQ stream, MessagePack + Gzip, dedupe by Annotation.Name) but no longer owns it — it runs in the new annotations/ .NET API submodule of the suite.

10. What survived into the new world

The following concepts are direct ports of the legacy WPF design and should be implemented in the React UI exactly the same way:

  • Module switcher with localized name + SVG icon → top navigation bar (Flights, Annotations, Dataset, Admin, Settings).
  • Detection-class strip with class colour, number, name, and PhotoMode switcher (Regular / Winter / Night, offsets 0/20/40). yoloId = classId + photoModeOffset.
  • Canvas editor: bounding-box draw / 8-handle resize / Ctrl multi-select / Ctrl+wheel zoom / Ctrl+drag pan / crosshair with active-class hint / normalized-coordinate clamping.
  • Annotation row gradient in the side list: a left-to-right gradient composed of each detection's class colour, opacity proportional to Confidence. Empty annotation → #40DDDDDD background.
  • Affiliation icons (Friendly / Hostile / Unknown / None) and combat readiness indicator (Ready / NotReady / Unknown) drawn next to the bounding-box label.
  • Time-windowed annotation rendering during video playback: Before = 50 ms, After = 150 ms, lookup via interval tree.
  • Frame-by-frame stepping in fixed counts (1, 5, 10, 30, 60), computed from 1 / fps.
  • Localized class names (DetectionClass.UIName carried alongside the English Name).
  • Camera config per session: altitude / focal length / sensor width drives GSD-based detection-size validation.
  • GPS-denied panel toggle under the canvas (now implemented as the GPS-Denied mode of the Flights page in the React UI).
  • Help window with the six annotation quality rules.
  • Color scheme: dark navy/blue primary (#343a40), orange accents (#fd7e14), dark gray background (#1e1e1e), green success (#40c057), blue primary buttons (#228be6), red danger (#fa5252).
  • Confirmation dialogs for delete-media / delete-selected / delete-all / deactivate-user.
  • Resizable panel widths persisted per user.

11. What is intentionally NOT being ported

  • The DI host inside the UI process. The React app does not own a service container, RabbitMQ consumer, SQLite database, or background worker. All of that now lives in the per-service .NET / Python / Cython submodules.
  • LibVLCSharp. The browser's native <video> element with a frame-accurate seeking shim handles playback.
  • ZeroMQ DealerSockets. The browser only speaks HTTP and SSE. Inference, GPS-matching, satellite tile fetching, and loader requests are all exposed as REST endpoints by their respective suite services.
  • The static Annotation.Init(...) initializer. Path/colour computation becomes selector logic over the API DTOs, with no static state.
  • The Azaion.Common god-assembly. Each concern is now a separate suite submodule with its own repo, Dockerfile, and OpenAPI document.
  • The Azaion.LoaderUI external-process handoff with encrypted creds on the command line. The browser performs POST /auth/login against the Admin API and stores a JWT.
  • The Cython Loader and the binary-split key-fragment dance. That whole protocol is server-side now (loader/ submodule) and the React UI is not involved beyond showing a progress screen.

12. How to read the research copy

cd /Users/obezdienie001/dev/azaion/suite/annotations-research
git status                  # detached at 22529c2 "Revert add MediaFile"
git log --oneline -n 5      # see surrounding commits

The folder is a plain clone of suite/annotations and is not wired into the suite's .gitmodules, so the parent repository ignores it.

If you want to compare the WPF-era code to the immediately following "big refactoring" commit, the comparison is:

git log --oneline --reverse 22529c2..e7ea5a8     # there is only e7ea5a8 itself
git diff 22529c2 e7ea5a8 -- Azaion.Annotator     # what the cleanup changed
git diff 22529c2 e7ea5a8 -- Azaion.Common        # the big assembly split prep

Two commits later (fbbe556 / 9e7dc29) the WPF projects disappear entirely and are replaced by a containerised .NET API — that is the state currently checked out in suite/annotations.