feat: add governed nationwide AOI orchestration and CUDA enforcement
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-26 05:23:33 +02:00
parent 25b6f1ab39
commit 2be72fac58
50 changed files with 1596 additions and 51 deletions
+7 -5
View File
@@ -108,7 +108,7 @@ interface PlannedOnDemandMapProduct extends OnDemandMapProduct {
function productSupportsSelection(product: OnDemandMapProduct, bbox: VectorSelectionBBox): boolean {
const scale = selectionAnalysisScale(bbox)
if (scale === 'overview') return false
if (scale === 'overview') return true
const dimensions = selectionDimensions(bbox)
if (product.kind === 'dhmv' || product.kind === 'spw_terrain' || product.kind === 'flood_hazard') {
return dimensions.areaSquareMetres <= 280_000_000
@@ -1563,10 +1563,10 @@ export function MapWorkspace({
const heightKm = dimensions.heightMetres / 1000
if (mapSelectionScale === 'regional') {
const partitionCount = splitSelectionBbox(mapSelectionBbox).length
return `Regionale selectie van ${widthKm.toLocaleString('nl-BE', { maximumFractionDigits: 1 })} x ${heightKm.toLocaleString('nl-BE', { maximumFractionDigits: 1 })} km. Het gekozen thema kan over maximaal ${partitionCount} begrensde bronpartities worden verwerkt; teken een kleiner gebied wanneer een fijnmazige rasterbron buiten het veilige pixelbudget valt.`
return `Regionale selectie van ${widthKm.toLocaleString('nl-BE', { maximumFractionDigits: 1 })} x ${heightKm.toLocaleString('nl-BE', { maximumFractionDigits: 1 })} km. De server verwerkt dit thema hervatbaar over ${partitionCount} of meer bronafhankelijke partities en presenteert één gezamenlijke status.`
}
if (mapSelectionScale === 'overview') {
return `Overzichtsselectie van ${widthKm.toLocaleString('nl-BE', { maximumFractionDigits: 1 })} x ${heightKm.toLocaleString('nl-BE', { maximumFractionDigits: 1 })} km. Het gekozen detailthema wordt niet automatisch op deze schaal bevraagd. Teken maximaal 50 x 50 km voor regionale thema's en ongeveer 16 x 16 km voor 5 m-rasters.`
return `Overzichtsselectie van ${widthKm.toLocaleString('nl-BE', { maximumFractionDigits: 1 })} x ${heightKm.toLocaleString('nl-BE', { maximumFractionDigits: 1 })} km. De server kiest automatisch bronlimieten, checkpoints en partities; resolutie en beschikbare dekking blijven die van de officiële bron.`
}
return null
}, [mapSelectionBbox, mapSelectionScale])
@@ -1946,11 +1946,11 @@ export function MapWorkspace({
resolvedZones = resolvedCoverage.intersected_zones
}
let resolvedProducts: PlannedOnDemandMapProduct[] = []
if (analysisMode === 'current' && scale !== 'overview') {
if (analysisMode === 'current') {
const zoneProducts = resolvedZones
? onDemandProductsForZones(resolvedZones)
: []
if (scale === 'detail' || !selectedProjectId) {
if (scale === 'detail' || !selectedProjectId || scale === 'overview') {
resolvedProducts = zoneProducts
.filter((product) => productSupportsSelection(product, bbox))
.map((product) => ({
@@ -2025,6 +2025,8 @@ export function MapWorkspace({
productKey: onDemandProduct.productKey,
displayName: onDemandProduct.displayName,
historyProductKeys: onDemandProduct.historyProductKeys,
coverageZone: onDemandProduct.coverageZones.find((zone) => resolvedZones?.includes(zone)),
serverOrchestrated: scale !== 'detail',
},
acquisitionBboxes: onDemandProduct.acquisitionBboxes,
featureLimit: resultFeatureLimit,
@@ -1,6 +1,9 @@
import type { ProviderCapability } from '../../types'
import { useCallback, useEffect, useState } from 'react'
import type { AoiOperation, ProviderCapability } from '../../types'
import { aoiOperationsApi } from '../../services/api'
interface ProviderPanelProps {
selectedProjectId: string | null
providers: ProviderCapability[]
loadingCapabilities: boolean
capabilitiesError: string | null
@@ -40,6 +43,7 @@ function providerLayerLabel(value: string): string {
}
export function ProviderPanel({
selectedProjectId,
providers,
loadingCapabilities,
capabilitiesError,
@@ -49,6 +53,32 @@ export function ProviderPanel({
onOpenMap,
}: ProviderPanelProps): JSX.Element {
const configuredCount = providers.filter((provider) => provider.configured).length
const [operations, setOperations] = useState<AoiOperation[]>([])
const [operationsError, setOperationsError] = useState<string | null>(null)
const [loadingOperations, setLoadingOperations] = useState(false)
const loadOperations = useCallback(async () => {
if (!selectedProjectId) {
setOperations([])
return
}
setLoadingOperations(true)
try {
const response = await aoiOperationsApi.list(selectedProjectId)
setOperations(response.items)
setOperationsError(null)
} catch (error) {
setOperationsError(error instanceof Error ? error.message : 'AOI-verwerking kon niet worden geladen.')
} finally {
setLoadingOperations(false)
}
}, [selectedProjectId])
useEffect(() => {
void loadOperations()
const timer = window.setInterval(() => void loadOperations(), 5000)
return () => window.clearInterval(timer)
}, [loadOperations])
return (
<section className="system-provider-panel">
<div className="system-provider-shell">
@@ -93,6 +123,31 @@ export function ProviderPanel({
) : null}
</div>
<section className="system-provider-capability-surface" aria-label="AOI-verwerkingsvoortgang">
<div className="panel-title-row">
<div>
<strong>Gebiedsverwerking</strong>
<p className="muted">Server-side partities, hervatbare voortgang en bronfouten voor grote selecties.</p>
</div>
<button type="button" className="secondary-action" onClick={() => void loadOperations()} disabled={loadingOperations || !selectedProjectId}>Vernieuwen</button>
</div>
{loadingOperations && operations.length === 0 ? <div className="result-state result-state-loading"><strong>Verwerkingen laden.</strong></div> : null}
{operationsError ? <div className="result-state result-state-error"><strong>Verwerkingsstatus niet bereikbaar.</strong><p>{operationsError}</p></div> : null}
{!loadingOperations && !operationsError && operations.length === 0 ? <div className="result-state result-state-empty"><strong>Nog geen gebiedsverwerking.</strong><p>Grote officiële bronselecties verschijnen hier met één gezamenlijke voortgang.</p></div> : null}
{operations.length > 0 ? (
<ul className="system-provider-list">
{operations.map((operation) => (
<li className="system-provider-card" key={operation.id}>
<div className="system-provider-header"><div><strong>{operation.operation_type}</strong><span>{String(operation.plan_json.provider_key ?? 'bron')} · {String(operation.plan_json.product_key ?? 'product')}</span></div><span className={operation.status === 'success' ? 'status-badge status-badge-ready' : 'status-badge'}>{operation.status}</span></div>
<progress value={operation.progress} max={1} aria-label={`Voortgang ${operation.operation_type}`} />
<div className="entity-meta"><span>{Math.round(operation.progress * 100)}%</span><span>{operation.partitions.length} partities</span><span>{operation.partition_counts.failed ?? 0} mislukt</span></div>
{operation.error_message ? <p className="inline-error">{operation.error_message}</p> : null}
</li>
))}
</ul>
) : null}
</section>
<div className="system-provider-capability-surface" aria-label="Provider capability registry">
<div className="provider-detail-stack">
<strong>Officiële referentiebronnen</strong>