Update
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-27 23:28:43 +02:00
parent 21015757bd
commit c76a746cd7
39 changed files with 3268 additions and 519 deletions
+105 -38
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { CircleAlert, LogOut, UserRound } from 'lucide-react'
import { CircleAlert, LogOut, ShieldCheck, UserRound } from 'lucide-react'
import '@fontsource/manrope/latin-500.css'
import '@fontsource/manrope/latin-600.css'
import '@fontsource/manrope/latin-700.css'
@@ -10,6 +10,7 @@ import './styles/app.css'
import './styles/premium.css'
import './styles/atlas-workbench.css'
import './styles/atlas-premium-v2.css'
import './styles/professionalization.css'
import { LandingPage } from './components/auth/LandingPage'
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
import { GeoAssistantPanel } from './components/assistant/GeoAssistantPanel'
@@ -86,20 +87,39 @@ const workspaceNavGroups: WorkspaceNavigationGroup[] = [
{ label: 'Beheer', keys: ['overview', 'system'] },
]
const guestWorkspaceKeys = new Set<WorkspaceKey>(['map', 'analysis'])
const guestWorkspaceGroups: WorkspaceNavigationGroup[] = [
{ label: 'Demowerkruimte', keys: ['map', 'analysis'] },
]
interface WorkbenchAppProps {
username: string | null
accessMode: 'open' | 'operator' | 'guest'
loggingOut: boolean
onLogout: () => void
}
function WorkbenchApp({ username, loggingOut, onLogout }: WorkbenchAppProps): JSX.Element {
function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchAppProps): JSX.Element {
const isGuest = accessMode === 'guest'
const [activeWorkspace, setActiveWorkspace] = useState<WorkspaceKey>('map')
const [inspectorOpen, setInspectorOpen] = useState(false)
const [guestDemoReady, setGuestDemoReady] = useState(!isGuest)
const guestDemoStartedRef = useRef(false)
const visibleWorkspaceItems = useMemo(
() => isGuest ? workspaceNavItems.filter((item) => guestWorkspaceKeys.has(item.key)) : workspaceNavItems,
[isGuest],
)
const visibleWorkspaceGroups = isGuest ? guestWorkspaceGroups : workspaceNavGroups
const [mapContentMode, setMapContentMode] = useState<'dataset' | 'analysis'>('dataset')
const [mapContextSourceLabel, setMapContextSourceLabel] = useState<string | null>(null)
const [mapContextLayerLabel, setMapContextLayerLabel] = useState<string | null>(null)
const workbenchMainRef = useRef<HTMLElement | null>(null)
const previousWorkspaceRef = useRef<WorkspaceKey>(activeWorkspace)
useEffect(() => {
if (isGuest && !guestWorkspaceKeys.has(activeWorkspace)) {
setActiveWorkspace('map')
}
}, [activeWorkspace, isGuest])
useEffect(() => {
workbenchMainRef.current?.scrollTo({ top: 0, left: 0 })
if (previousWorkspaceRef.current !== activeWorkspace) {
@@ -566,6 +586,7 @@ function WorkbenchApp({ username, loggingOut, onLogout }: WorkbenchAppProps): JS
demoWorkflowMessage,
loadDemoWorkflow,
} = useDemoWorkflow({
restrictedMode: isGuest,
loadProjects,
loadProjectData,
loadDatasetDetails,
@@ -586,7 +607,15 @@ function WorkbenchApp({ username, loggingOut, onLogout }: WorkbenchAppProps): JS
setErrorMessage,
})
useEffect(() => {
if (!isGuest || guestDemoStartedRef.current) return
guestDemoStartedRef.current = true
setGuestDemoReady(false)
void loadDemoWorkflow().finally(() => setGuestDemoReady(true))
}, [isGuest, loadDemoWorkflow])
useWorkbenchBootstrap({
restrictedMode: isGuest,
selectedProjectId,
selectedDetectionRunId,
detectionClassFilter,
@@ -613,7 +642,7 @@ function WorkbenchApp({ username, loggingOut, onLogout }: WorkbenchAppProps): JS
})
const selectedArea = areas.find((area) => area.id === selectedMapAreaId) ?? null
const activeWorkspaceItem = workspaceNavItems.find((item) => item.key === activeWorkspace) ?? workspaceNavItems[0]
const activeWorkspaceItem = visibleWorkspaceItems.find((item) => item.key === activeWorkspace) ?? visibleWorkspaceItems[0]
const mapLayerSourceLabel = useMemo(() => {
if (analysisMapLayerActive && changeDetectionResult?.geojson) {
return 'Veranderingsanalyse'
@@ -687,6 +716,10 @@ function WorkbenchApp({ username, loggingOut, onLogout }: WorkbenchAppProps): JS
setActiveWorkspace('exports')
}
const openWorkflowGuidanceStep = (target: WorkspaceKey) => {
if (isGuest && !guestWorkspaceKeys.has(target)) {
setActiveWorkspace(target === 'exports' ? 'analysis' : 'map')
return
}
if (target === 'map' && availableMapDatasets.length > 0 && !mapFeatureCollection) {
openDatasetInMap(availableMapDatasets[0])
return
@@ -781,7 +814,7 @@ function WorkbenchApp({ username, loggingOut, onLogout }: WorkbenchAppProps): JS
: 'Geen actieve laag'
return (
<div className="app-shell workbench-shell">
<div className={isGuest ? "app-shell workbench-shell workbench-shell-guest" : "app-shell workbench-shell"}>
<a
className="skip-link"
href="#workspace-main"
@@ -805,8 +838,8 @@ function WorkbenchApp({ username, loggingOut, onLogout }: WorkbenchAppProps): JS
<div className="workbench-layout">
<WorkbenchNavigation
activeWorkspace={activeWorkspace}
groups={workspaceNavGroups}
items={workspaceNavItems}
groups={visibleWorkspaceGroups}
items={visibleWorkspaceItems}
onSelect={setActiveWorkspace}
/>
@@ -838,18 +871,39 @@ function WorkbenchApp({ username, loggingOut, onLogout }: WorkbenchAppProps): JS
<span aria-hidden="true" />
<strong>{workspaceDataLoading ? 'Laden' : errorMessage ? 'Aandacht' : 'Gereed'}</strong>
</div>
{username ? (
<div className="context-account" aria-label="Aangemelde gebruiker">
<UserRound aria-hidden="true" />
<span title={username}>{username}</span>
{username || isGuest ? (
<div
className={isGuest ? 'context-account context-account-guest' : 'context-account'}
aria-label={isGuest ? 'Tijdelijke gastensessie' : 'Aangemelde gebruiker'}
>
{isGuest ? <ShieldCheck aria-hidden="true" /> : <UserRound aria-hidden="true" />}
<span title={username ?? undefined}>{isGuest ? 'Gastmodus' : username}</span>
<button type="button" onClick={onLogout} disabled={loggingOut}>
<LogOut aria-hidden="true" />
<span>{loggingOut ? 'Uitloggen' : 'Uitloggen'}</span>
<span>{loggingOut ? 'Afsluiten…' : isGuest ? 'Demo afsluiten' : 'Uitloggen'}</span>
</button>
</div>
) : null}
</header>
{isGuest ? (
<div className="guest-mode-banner" role="status">
<ShieldCheck aria-hidden="true" />
<div>
<strong>Tijdelijke demowerkruimte</strong>
<span>U verkent vooraf geladen voorbeelddata. Wijzigingen, nieuwe analyses en operatorfuncties zijn uitgeschakeld.</span>
</div>
<span className="guest-mode-badge">Alleen-lezen</span>
</div>
) : null}
{isGuest && (!guestDemoReady || loadingDemoWorkflow) ? (
<div className="guest-demo-loading" role="status" aria-live="polite">
<span aria-hidden="true" />
<strong>Demodata en kwaliteitsbewijs worden voorbereid</strong>
</div>
) : null}
<div className="workbench-content">
<main
@@ -867,7 +921,7 @@ function WorkbenchApp({ username, loggingOut, onLogout }: WorkbenchAppProps): JS
<WorkspaceSignal workspace={activeWorkspace} />
<div className="workspace-heading-actions">
<p>{activeWorkspaceItem.description}</p>
{activeWorkspace !== 'overview' ? (
{!isGuest && activeWorkspace !== 'overview' ? (
<button
type="button"
className={inspectorOpen ? 'inspector-toggle inspector-toggle-active' : 'inspector-toggle'}
@@ -950,6 +1004,7 @@ function WorkbenchApp({ username, loggingOut, onLogout }: WorkbenchAppProps): JS
<div className="workspace-persistent-map" hidden={activeWorkspace !== 'map'}>
<MapWorkspace
readOnly={isGuest}
selectedProjectId={selectedProjectId}
projects={projects}
areas={areas}
@@ -1045,8 +1100,8 @@ function WorkbenchApp({ username, loggingOut, onLogout }: WorkbenchAppProps): JS
onRefreshProjectData={() => (
selectedProjectId ? loadProjectData(selectedProjectId) : Promise.resolve(null)
)}
onOpenAssistant={() => setActiveWorkspace('assistant')}
onOpenExports={() => setActiveWorkspace('exports')}
onOpenAssistant={() => setActiveWorkspace(isGuest ? 'analysis' : 'assistant')}
onOpenExports={() => setActiveWorkspace(isGuest ? 'analysis' : 'exports')}
/>
</div>
@@ -1063,29 +1118,39 @@ function WorkbenchApp({ username, loggingOut, onLogout }: WorkbenchAppProps): JS
evidenceLoading={qualityEvidenceLoading}
evidenceError={qualityEvidenceError}
onOpenMapWorkspace={() => setActiveWorkspace('map')}
onOpenAnalysisWorkspace={() => setActiveWorkspace('ai')}
onOpenAnalysisWorkspace={() => setActiveWorkspace(isGuest ? 'map' : 'ai')}
/>
<details className="secondary-analysis-disclosure">
<summary>
<span>Historische vectorlagen vergelijken</span>
<strong>Geavanceerd</strong>
</summary>
<ChangeDetectionPanel
vectorDatasets={availableVectorDatasets}
sourceDatasetId={changeSourceDatasetId}
targetDatasetId={changeTargetDatasetId}
iouThreshold={changeIouThreshold}
includeUnchanged={changeIncludeUnchanged}
running={runningChangeDetection}
result={changeDetectionResult}
error={changeDetectionError}
onSourceDatasetChange={setChangeSourceDatasetId}
onTargetDatasetChange={setChangeTargetDatasetId}
onIouThresholdChange={setChangeIouThreshold}
onIncludeUnchangedChange={setChangeIncludeUnchanged}
onRun={runChangeDetection}
/>
</details>
{!isGuest ? (
<details className="secondary-analysis-disclosure">
<summary>
<span>Historische vectorlagen vergelijken</span>
<strong>Geavanceerd</strong>
</summary>
<ChangeDetectionPanel
vectorDatasets={availableVectorDatasets}
sourceDatasetId={changeSourceDatasetId}
targetDatasetId={changeTargetDatasetId}
iouThreshold={changeIouThreshold}
includeUnchanged={changeIncludeUnchanged}
running={runningChangeDetection}
result={changeDetectionResult}
error={changeDetectionError}
onSourceDatasetChange={setChangeSourceDatasetId}
onTargetDatasetChange={setChangeTargetDatasetId}
onIouThresholdChange={setChangeIouThreshold}
onIncludeUnchangedChange={setChangeIncludeUnchanged}
onRun={runChangeDetection}
/>
</details>
) : (
<div className="guest-readonly-card">
<ShieldCheck aria-hidden="true" />
<div>
<strong>Controleerbaar voorbeeldresultaat</strong>
<span>Deze kwaliteitsweergave gebruikt de vooraf berekende demo. Nieuwe vergelijkingen zijn alleen beschikbaar voor operators.</span>
</div>
</div>
)}
</div>
) : null}
@@ -1250,7 +1315,7 @@ function WorkbenchApp({ username, loggingOut, onLogout }: WorkbenchAppProps): JS
) : null}
</main>
{inspectorOpen ? (
{inspectorOpen && !isGuest ? (
<aside className="workbench-inspector" id="workbench-inspector" aria-label="Details van de huidige selectie">
<WorkbenchInspector
selectedProject={selectedProject}
@@ -1357,6 +1422,7 @@ function App(): JSX.Element {
return (
<LandingPage
serviceError={sessionError}
guestAccessEnabled={session.guest_access_enabled}
onAuthenticated={handleAuthenticated}
/>
)
@@ -1364,7 +1430,8 @@ function App(): JSX.Element {
return (
<WorkbenchApp
username={session.authentication_required ? session.username : null}
username={session.username}
accessMode={session.role ?? (session.authentication_required ? 'operator' : 'open')}
loggingOut={loggingOut}
onLogout={handleLogout}
/>