Files
satellite-provider/_docs/02_document/system-flows.md
T
Oleksandr Bezdieniezhnykh bc04ba7f99 [AZ-794] [AZ-795] [AZ-796] Cycle 7 Steps 12-15 sync (test-spec / docs / security / perf)
Step 12 (Test-Spec Sync): adds BT-27 for the AZ-796 9-rule
validation surface and 12 cycle-7 AC rows + Coverage Summary
update to traceability-matrix.md.

Step 13 (Update Docs): module-layout + module docs for the new
SatelliteProvider.Api/Validators namespace + GlobalExceptionHandler
+ updated TileInventory DTO; tests_unit + tests_integration
document the new InventoryRequestValidatorTests (16 unit tests
covering all 9 rules) + TileInventoryValidationTests (16
integration tests) + ProblemDetailsAssertions support;
glossary entries for Validation Problem Details / FluentValidation
/ Unmapped Member Handling; system-flows F8 (Tile Inventory Bulk
Lookup) expanded with deserializer + validator gates and a 13-row
Validation Surface table; data_parameters § Tile Inventory
documents the v2 input schema + constraints; ripple_log_cycle7
captures the doc-side ripple decisions.

Step 14 (Security Audit): 5-phase audit ran; verdict
PASS_WITH_WARNINGS (3 Low findings — D-AZ795-1 FluentValidation
12.0.0 -> 12.1.1 recommended bump, F-AZ795-1 JsonException.Message
leak in 400 detail, F-AZ795-2 BadHttpRequestException.Message leak).
No Critical / High; auth runs before validation (confirmed in
Program.cs); two NuGet additions (FluentValidation 12.0.0 +
.DependencyInjectionExtensions 12.0.0) both CVE-clean. Per-phase
reports plus consolidated security_report_cycle7.md.

Step 15 (Performance Test): docker compose stack used for perf
run, scripts/run-performance-tests.sh exited 0 with 8/8 scenarios
PASS (second consecutive clean exit-0); added PT-09 cycle-7 smoke
probe (v2 z/x/y schema, 2500-tile all-miss batch) measuring
min=27ms median=44ms p95=73ms max=86ms (13.7x under AZ-505 AC-4
1000ms budget). PT-07/08 improvements traced to the cycle-6 TLS
handshake-overhead identification, not application-side change.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 11:24:27 +03:00

17 KiB

Satellite Provider — System Flows

Flow Inventory

# Flow Name Trigger Primary Components Criticality
F1 Single Tile Download HTTP GET /api/satellite/tiles/latlon WebApi, TileDownloader, DataAccess High
F2 Region Request HTTP POST /api/satellite/request WebApi, RegionProcessing, TileDownloader, DataAccess High
F3 Region Processing Queue dequeue (background) RegionProcessing, TileDownloader, DataAccess High
F4 Route Creation HTTP POST /api/satellite/route WebApi, RouteManagement, DataAccess High
F5 Route Map Processing Queue dequeue (background) RouteManagement, RegionProcessing, TileDownloader, DataAccess Medium
F6 Status Query HTTP GET /api/satellite/region/{id} or /route/{id} WebApi, DataAccess Low
F7 Leaflet Tile Serving HTTP GET /tiles/{z}/{x}/{y} WebApi, TileService, DataAccess, FileSystem High
F8 Tile Inventory Bulk Lookup HTTP POST /api/satellite/tiles/inventory WebApi, TileService, DataAccess High

Flow Dependencies

Flow Depends On Shares Data With
F1 F3 (tile cache)
F2 F3 (triggers it)
F3 F2 enqueues work F1 (shares tile cache), F5
F4 F5 (triggers it)
F5 F4 must create route first F3 (submits region requests)
F6 F2/F4 must exist
F7 F1 or F3 must have populated the tile (else 404) F1, F3, F8 (shares tiles.location_hash)
F8 F1, F3, F7 (shares tiles.location_hash)

Flow F1: Single Tile Download

Description

Client requests a single satellite tile by geographic coordinates and zoom level. The service checks the cache (DB), downloads from Google Maps if not cached, stores it, and returns metadata.

Preconditions

  • Valid latitude, longitude, and zoom level provided
  • Google Maps session token configured

Sequence Diagram

sequenceDiagram
    participant Client
    participant WebApi
    participant TileService
    participant TileRepo
    participant GoogleMaps
    participant FileSystem

    Client->>WebApi: GET /api/satellite/tiles/latlon?lat&lon&zoom
    WebApi->>TileService: DownloadTileAsync(lat, lon, zoom)
    TileService->>TileRepo: FindByCoordinates(lat, lon, zoom)
    alt Tile exists in cache
        TileRepo-->>TileService: TileEntity
        TileService-->>WebApi: TileMetadata (cached)
    else Not cached
        TileService->>GoogleMaps: Download tile image
        GoogleMaps-->>TileService: JPEG bytes
        TileService->>FileSystem: Save to ./tiles/{zoom}/{x}/{y}.jpg
        TileService->>TileRepo: Insert(TileEntity)
        TileService-->>WebApi: TileMetadata (new)
    end
    WebApi-->>Client: JSON response

Error Scenarios

Error Where Detection Recovery
Google Maps timeout Download step HttpClient timeout Return error to caller
Duplicate download race Concurrent requests ConcurrentDictionary check Await existing download
Disk full File save IOException Exception propagated, region fails

Flow F2: Region Request

Description

Client submits a region definition (center point, size, zoom). The request is persisted and queued for asynchronous processing.

Preconditions

  • Valid region parameters (lat, lon, size_meters, zoom_level)

Sequence Diagram

sequenceDiagram
    participant Client
    participant WebApi
    participant RegionService
    participant RegionRepo
    participant Queue

    Client->>WebApi: POST /api/satellite/request {lat, lon, size, zoom}
    WebApi->>RegionService: CreateRegionRequest(dto)
    RegionService->>RegionRepo: Insert(RegionEntity status=pending)
    RegionRepo-->>RegionService: region_id
    RegionService->>Queue: Enqueue(region_id)
    RegionService-->>WebApi: region_id
    WebApi-->>Client: 200 OK {region_id}

Error Scenarios

Error Where Detection Recovery
Queue full Enqueue step Channel at capacity Return 503 / reject request
DB insert failure Persist step Exception Return 500

Flow F3: Region Processing (Background)

Description

Background service dequeues region IDs, calculates tile grid, downloads all tiles (with concurrency control), optionally stitches them, and produces output files (CSV, summary, stitched image).

Preconditions

  • Region exists in DB with status "pending"
  • Google Maps session token configured

Sequence Diagram

sequenceDiagram
    participant Queue
    participant RegionProcessor
    participant RegionService
    participant TileService
    participant GoogleMaps
    participant RegionRepo
    participant FileSystem

    Queue->>RegionProcessor: Dequeue region_id
    RegionProcessor->>RegionRepo: GetById(region_id)
    RegionProcessor->>RegionRepo: UpdateStatus(processing)
    loop For each tile in grid
        RegionProcessor->>TileService: DownloadTileAsync(lat, lon, zoom)
        TileService->>GoogleMaps: Download (if not cached)
    end
    RegionProcessor->>FileSystem: Write CSV (tile manifest)
    RegionProcessor->>FileSystem: Write summary file
    opt stitch_tiles = true
        RegionProcessor->>FileSystem: Stitch tiles into composite image
    end
    RegionProcessor->>RegionRepo: UpdateStatus(completed, file paths)

Data Flow

Step From To Data Format
1 Queue RegionProcessor region_id int
2 RegionProcessor TileService lat, lon, zoom per tile method call
3 TileService FileSystem JPEG image file
4 RegionProcessor FileSystem tile manifest CSV
5 RegionProcessor FileSystem region summary TXT
6 RegionProcessor FileSystem composite image JPEG

Error Scenarios

Error Where Detection Recovery
Tile download failure Per-tile loop Exception from TileService Log, continue with remaining tiles
All tiles fail After loop Zero tiles downloaded Mark region as "failed"
Stitch failure Image processing ImageSharp exception Mark region failed, tiles still available

Flow F4: Route Creation

Description

Client submits a route (ordered waypoints + optional geofence polygons). The service interpolates intermediate points every ~200m and persists the full point set.

Preconditions

  • At least 2 waypoints provided
  • Valid geofence polygons (if provided)

Sequence Diagram

sequenceDiagram
    participant Client
    participant WebApi
    participant RouteService
    participant RouteRepo
    participant GeoUtils

    Client->>WebApi: POST /api/satellite/route {points, geofences, options}
    WebApi->>RouteService: CreateRoute(request)
    RouteService->>GeoUtils: Interpolate points between waypoints
    GeoUtils-->>RouteService: All points (original + intermediate)
    RouteService->>RouteRepo: InsertRoute(RouteEntity)
    RouteService->>RouteRepo: InsertPoints(RoutePointEntities)
    RouteService-->>WebApi: RouteResponse
    WebApi-->>Client: 200 OK {route_id, total_points, total_distance}

Error Scenarios

Error Where Detection Recovery
Invalid points (< 2) Validation Count check Return 400
DB insert failure Persist step Exception Return 500

Flow F5: Route Map Processing (Background)

Description

When a route requests map tiles (request_maps = true), a background service creates region requests for each route point, optionally filtered by geofence, then waits for all regions to complete and produces a ZIP archive.

Preconditions

  • Route exists with request_maps = true
  • Route points already interpolated and persisted

Sequence Diagram

sequenceDiagram
    participant RouteProcessor
    participant RouteRepo
    participant RegionService
    participant Queue
    participant RegionProcessor
    participant FileSystem

    RouteProcessor->>RouteRepo: GetRouteWithPoints(route_id)
    loop For each route point
        RouteProcessor->>RouteProcessor: Check geofence (point-in-polygon)
        opt Point inside geofence (or no geofence)
            RouteProcessor->>RegionService: CreateRegionRequest(point)
            RegionService->>Queue: Enqueue(region_id)
        end
    end
    RouteProcessor->>RouteProcessor: Wait for all regions to complete
    opt create_tiles_zip = true
        RouteProcessor->>FileSystem: Create ZIP of all tiles (max 50MB)
        RouteProcessor->>RouteRepo: Update tiles_zip_path
    end

Error Scenarios

Error Where Detection Recovery
Region processing timeout Wait loop Polling timeout Mark route partially complete
ZIP exceeds 50MB ZIP creation Size check during write Truncate or skip
Geofence calculation error Point-in-polygon Exception Include point (fail-open)

Flow F6: Status Query

Description

Client polls for the status of a region or route by ID. Returns current processing state and output file paths when complete.

Sequence Diagram

sequenceDiagram
    participant Client
    participant WebApi
    participant DataAccess

    Client->>WebApi: GET /api/satellite/region/{id}
    WebApi->>DataAccess: GetRegionById(id)
    DataAccess-->>WebApi: RegionEntity (status, file paths)
    WebApi-->>Client: JSON {status, files}

Flow F7: Leaflet Tile Serving (added AZ-310, rewired AZ-505)

Description

Leaflet (or any HTTP/2-capable client) requests a single tile body by slippy (z, x, y). The handler resolves the most-recent variant across sources/flights by location_hash and streams the JPEG body back. AZ-505 rewired the lookup predicate from (tile_zoom, tile_x, tile_y) to location_hash = Uuidv5(TileNamespace, "{z}/{x}/{y}") so the read hits the tiles_leaflet_path covering index as an Index Only Scan; the selection rule (captured_at DESC, updated_at DESC, id DESC LIMIT 1) is unchanged from AZ-484 / AZ-503-foundation. Kestrel runs Http1AndHttp2 over TLS (https://+:8080 in dev) so ALPN multiplexes many concurrent leaflet requests on a single TLS connection.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Kestrel
    participant ServeTile
    participant TileService
    participant TileRepo
    participant FileSystem

    Client->>Kestrel: GET /tiles/{z}/{x}/{y} (HTTP/2 via TLS+ALPN)
    Kestrel->>ServeTile: route match
    ServeTile->>TileService: GetOrDownloadTileAsync(z, x, y)
    TileService->>TileRepo: GetByTileCoordinatesAsync(z, x, y)
    Note over TileRepo: WHERE location_hash = $1<br/>ORDER BY captured_at DESC, updated_at DESC, id DESC<br/>LIMIT 1<br/>Index Only Scan: tiles_leaflet_path
    alt Cached
        TileRepo-->>TileService: TileEntity
        TileService->>FileSystem: read file_path
        FileSystem-->>TileService: JPEG bytes
        TileService-->>ServeTile: TileBytes (file_path, ETag, Cache-Control)
        ServeTile-->>Client: 200 OK, JPEG body
    else Miss
        TileService->>TileService: download via GoogleMapsDownloaderV2
        Note over TileService: persists row + on-disk path, falls through to ServeTile
        TileService-->>ServeTile: TileBytes
        ServeTile-->>Client: 200 OK, JPEG body
    end

Error Handling

Failure Detection Handling
Tile not present and downloader rejects (404 from Google Maps) TileService.GetOrDownloadTileAsync propagates the downloader's HttpRequestException Returns 500; ServeTile does NOT translate this to 404 because the predicate matched (path-traversal cases below 404 earlier in routing)
Path traversal in /tiles/... segment ASP.NET Core route binding 400/404 before the handler runs (covered by SEC-02)

Flow F8: Tile Inventory Bulk Lookup (added AZ-505; renamed + strict-validated AZ-794+AZ-795+AZ-796, cycle 7)

Description

Programmatic clients (httpx http2=True, .NET HttpClient, onboard cross-repo callers) post a batch of up to 5000 {z, x, y} triples (Form A; the wire field names were renamed from tileZoom/tileX/tileY by AZ-794, cycle 7) or up to 5000 pre-computed location_hash UUIDs (Form B) and get one inventory entry per input slot, in the same order. Each entry says whether the cell is present and — when present — the most-recent row's id, capturedAt, source, flightId, and resolutionMPerPx. No tile bodies are returned; the caller subsequently fetches bodies via F7. This is the read-half of the bulk-list contract that the onboard gps-denied-onboard workspace consumes to decide which Google-Maps cells it needs and which UAV variants are already on the server.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Kestrel
    participant GetTilesInventory
    participant TileService
    participant TileRepo

    Client->>Kestrel: POST /api/satellite/tiles/inventory (JWT, Form A or B)
    Kestrel->>GetTilesInventory: route match
    Note over Kestrel,GetTilesInventory: AZ-795 deserializer guards (UnmappedMemberHandling.Disallow + [JsonRequired])<br/>catch unknown / missing / type-mismatched fields → 400 ValidationProblemDetails<br/>via GlobalExceptionHandler (cycle 7)
    GetTilesInventory->>GetTilesInventory: ValidationEndpointFilter<TileInventoryRequest><br/>(InventoryRequestValidator — AZ-796 cycle 7)
    Note over GetTilesInventory: 9 business rules: XOR / non-empty / per-array cap / Z range / X+Y range
    GetTilesInventory->>TileService: GetInventoryAsync(request)
    Note over TileService: Form A: compute location_hash per coord<br/>via Uuidv5.LocationHashForTile<br/>Form B: echo caller-supplied hashes
    TileService->>TileRepo: GetTilesByLocationHashesAsync(hashes)
    Note over TileRepo: NpgsqlCommand:<br/>SELECT DISTINCT ON (location_hash) ...<br/>WHERE location_hash = ANY($1::uuid[])<br/>ORDER BY location_hash, captured_at DESC, updated_at DESC, id DESC<br/>(bypasses Dapper IEnumerable expansion)
    TileRepo-->>TileService: IReadOnlyDictionary<Guid, TileEntity>
    TileService->>TileService: shape into TileInventoryEntry[] in request order
    TileService-->>GetTilesInventory: TileInventoryResponse
    GetTilesInventory-->>Client: 200 OK, JSON (results in input order)

Validation Surface (post-cycle 7 — AZ-795 + AZ-796)

Input Detection Response
Empty / missing body System.Text.Json ([JsonRequired] on Tiles/LocationHashes covered indirectly via InventoryRequestValidator) 400 + ValidationProblemDetails
Both tiles and locationHashes populated InventoryRequestValidator .Must(...) (Rule 1, XOR — tile-inventory.md v2.0.0 Inv-1) 400 + ValidationProblemDetails, key ""
Neither populated InventoryRequestValidator .Must(...) (Rule 1, XOR) 400 + ValidationProblemDetails, key ""
tiles or locationHashes array is empty InventoryRequestValidator .Must(...) (Rule 1, XOR — non-empty arm) 400 + ValidationProblemDetails, key ""
tiles.Count > 5000 (TileInventoryLimits.MaxEntriesPerRequest) InventoryRequestValidator .Must(t => t.Count <= 5000) (Rule 6 — Inv-7) 400 + ValidationProblemDetails, key tiles
locationHashes.Count > 5000 InventoryRequestValidator .Must(h => h.Count <= 5000) (Rule 7 — Inv-7) 400 + ValidationProblemDetails, key locationHashes
tiles[i].z missing OR out of range (must be 0..22 inclusive) [JsonRequired] on Z (deserializer) + TileCoordValidator Rule 4 400 + ValidationProblemDetails, key tiles[i].z
tiles[i].x missing OR < 0 OR >= 2^z [JsonRequired] on X (deserializer) + TileCoordValidator Rule 5 400 + ValidationProblemDetails, key tiles[i].x
tiles[i].y missing OR < 0 OR >= 2^z [JsonRequired] on Y (deserializer) + TileCoordValidator Rule 5 400 + ValidationProblemDetails, key tiles[i].y
Legacy tileZoom/tileX/tileY field names UnmappedMemberHandling.Disallow (deserializer; AZ-794 + AZ-795) 400 + ValidationProblemDetails, key tiles[i].tileZoom (etc.)
Unknown root or nested field UnmappedMemberHandling.Disallow (deserializer; AZ-795) 400 + ValidationProblemDetails, key on the unknown path
Wrong JSON type (e.g. "z": "18") System.Text.Json type-mismatch (deserializer; AZ-795) 400 + ValidationProblemDetails, key on the offending path
No Authorization: Bearer … header .RequireAuthorization() 401 before handler runs

All 4xx bodies conform to error-shape.md v1.0.0. The same ValidationProblemDetails shape is emitted whether the failure was caught by the FluentValidation business-rule layer (InventoryRequestValidator) or by the deserializer layer (via GlobalExceptionHandler). Both layers are unit + integration tested in SatelliteProvider.Tests/Validators/InventoryRequestValidatorTests and SatelliteProvider.IntegrationTests/TileInventoryValidationTests.

Performance

p95 ≤ 1000 ms for 2500-coord batches (AZ-505 AC-4). Cycle-6 measured: p95=66ms — well under budget. The covering index (tiles_leaflet_path) supplies the leading location_hash lookup; the projection's columns beyond the INCLUDE list (id, captured_at, flight_id, ...) trigger a bounded heap fetch which is documented and accepted per the AZ-505 NFR.