Schema enhancements (443 files): - Add class_uri with proper ontology references (schema:, prov:, skos:, rico:) - Add close_mappings, related_mappings per Rule 50 convention - Replace stub hc: slot_uri with standard predicates (dcterms:identifier, skos:prefLabel) - Improve descriptions with ontology mapping rationale - Add prefixes blocks to all schema modules Entity Resolution improvements: - Add entity_resolution module with email semantics parsing - Enhance build_entity_resolution.py with email-based matching signals - Extend Entity Review API with filtering by signal types and count - Add candidates caching and indexing for performance - Add ReviewLoginPage component New rules and documentation: - Add Rule 51: No Hallucinated Ontology References - Add .opencode/rules/no-hallucinated-ontology-references.md - Add .opencode/rules/slot-ontology-mapping-reference.md - Add adms.ttl and dqv.ttl ontology files Frontend ontology support: - Add RiC-O_1-1.rdf and schemaorg.owl to public/ontology
268 lines
8.4 KiB
TypeScript
268 lines
8.4 KiB
TypeScript
/**
|
||
* Main Application Component with Routing and Authentication
|
||
*
|
||
* © 2025 Netwerk Digitaal Erfgoed & TextPast. All rights reserved.
|
||
*/
|
||
|
||
import React, { Suspense, useEffect, useState, useCallback } from 'react';
|
||
import {
|
||
createBrowserRouter,
|
||
RouterProvider,
|
||
Navigate,
|
||
} from 'react-router-dom';
|
||
import { AuthProvider } from './contexts/AuthContext';
|
||
import { LanguageProvider } from './contexts/LanguageContext';
|
||
import { UIStateProvider } from './contexts/UIStateContext';
|
||
import { ProtectedRoute } from './components/ProtectedRoute';
|
||
import { Layout } from './components/layout/Layout';
|
||
import { preloadInstitutions } from './hooks/useGeoApiInstitutions';
|
||
import {
|
||
lazyWithRetry,
|
||
RouterErrorBoundary,
|
||
LazyLoadErrorBoundary
|
||
} from './components/common/LazyLoadError';
|
||
import { startVersionCheck } from './lib/version-check';
|
||
import { RefreshCw } from 'lucide-react';
|
||
|
||
// Eagerly loaded pages (small, frequently accessed)
|
||
import { LoginPage } from './pages/LoginPage';
|
||
import { Settings } from './pages/Settings';
|
||
import ProjectPlanPage from './pages/ProjectPlanPage';
|
||
|
||
// Lazy loaded pages with automatic retry on chunk load failures
|
||
// These use lazyWithRetry() to handle deployment-related chunk errors gracefully
|
||
// Map page - imports maplibre-gl (~1MB)
|
||
const NDEMapPage = lazyWithRetry(() => import('./pages/NDEMapPageMapLibre'));
|
||
// Visualize page - imports mermaid, d3, elkjs (~1.5MB combined)
|
||
const Visualize = lazyWithRetry(() => import('./pages/Visualize').then(m => ({ default: m.Visualize })));
|
||
// LinkML viewer - large schema parsing
|
||
const LinkMLViewerPage = lazyWithRetry(() => import('./pages/LinkMLViewerPage'));
|
||
// Ontology viewer - imports visualization libraries
|
||
const OntologyViewerPage = lazyWithRetry(() => import('./pages/OntologyViewerPage'));
|
||
// Data mappings page - custodian data mappings explorer
|
||
const DataMapPage = lazyWithRetry(() => import('./pages/DataMapPage'));
|
||
// Query builder - medium complexity
|
||
const QueryBuilderPage = lazyWithRetry(() => import('./pages/QueryBuilderPage'));
|
||
// Database page
|
||
const Database = lazyWithRetry(() => import('./pages/Database').then(m => ({ default: m.Database })));
|
||
// Stats page
|
||
const NDEStatsPage = lazyWithRetry(() => import('./pages/NDEStatsPage'));
|
||
// Conversation page
|
||
const ConversationPage = lazyWithRetry(() => import('./pages/ConversationPage'));
|
||
// Institution browser page
|
||
const InstitutionBrowserPage = lazyWithRetry(() => import('./pages/InstitutionBrowserPage'));
|
||
// Entity resolution review page
|
||
const EntityReviewPage = lazyWithRetry(() => import('./pages/EntityReviewPage'));
|
||
// Review login page (separate auth from main app)
|
||
const ReviewLoginPage = lazyWithRetry(() => import('./pages/ReviewLoginPage'));
|
||
|
||
import './App.css';
|
||
|
||
// Loading fallback component for lazy-loaded pages
|
||
const PageLoader = () => (
|
||
<div className="flex items-center justify-center h-64">
|
||
<div className="text-center">
|
||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||
<p className="text-gray-600 dark:text-gray-400">Loading...</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
// Wrap lazy components with Suspense and error boundary for chunk load errors
|
||
const withSuspense = (Component: React.LazyExoticComponent<React.ComponentType>) => (
|
||
<LazyLoadErrorBoundary>
|
||
<Suspense fallback={<PageLoader />}>
|
||
<Component />
|
||
</Suspense>
|
||
</LazyLoadErrorBoundary>
|
||
);
|
||
|
||
// Create router configuration with protected and public routes
|
||
// Documentation routes (linkml, datamap, ontology, browse) are PUBLIC for embedding
|
||
// Other pages require authentication via ProtectedRoute
|
||
const router = createBrowserRouter([
|
||
{
|
||
path: '/login',
|
||
element: <LoginPage />,
|
||
errorElement: <RouterErrorBoundary />,
|
||
},
|
||
{
|
||
// Dedicated login for review page (separate credentials)
|
||
path: '/review-login',
|
||
element: withSuspense(ReviewLoginPage),
|
||
errorElement: <RouterErrorBoundary />,
|
||
},
|
||
// ========================================================================
|
||
// PUBLIC ROUTES - No authentication required
|
||
// These can be embedded in iframes from other domains (e.g., archief.support)
|
||
// ========================================================================
|
||
{
|
||
path: '/linkml',
|
||
element: <Layout />,
|
||
errorElement: <RouterErrorBoundary />,
|
||
children: [
|
||
{
|
||
index: true,
|
||
element: withSuspense(LinkMLViewerPage),
|
||
},
|
||
],
|
||
},
|
||
{
|
||
path: '/datamap',
|
||
element: <Layout />,
|
||
errorElement: <RouterErrorBoundary />,
|
||
children: [
|
||
{
|
||
index: true,
|
||
element: withSuspense(DataMapPage),
|
||
},
|
||
],
|
||
},
|
||
{
|
||
path: '/ontology',
|
||
element: <Layout />,
|
||
errorElement: <RouterErrorBoundary />,
|
||
children: [
|
||
{
|
||
index: true,
|
||
element: withSuspense(OntologyViewerPage),
|
||
},
|
||
],
|
||
},
|
||
{
|
||
path: '/browse',
|
||
element: <Layout />,
|
||
errorElement: <RouterErrorBoundary />,
|
||
children: [
|
||
{
|
||
index: true,
|
||
element: withSuspense(InstitutionBrowserPage),
|
||
},
|
||
],
|
||
},
|
||
// ========================================================================
|
||
// PROTECTED ROUTES - Authentication required
|
||
// ========================================================================
|
||
{
|
||
path: '/',
|
||
element: (
|
||
<ProtectedRoute>
|
||
<Layout />
|
||
</ProtectedRoute>
|
||
),
|
||
errorElement: <RouterErrorBoundary />,
|
||
children: [
|
||
{
|
||
// Home page redirects to LinkML viewer
|
||
index: true,
|
||
element: <Navigate to="/linkml" replace />,
|
||
},
|
||
{
|
||
path: 'visualize',
|
||
element: withSuspense(Visualize),
|
||
},
|
||
{
|
||
path: 'database',
|
||
element: withSuspense(Database),
|
||
},
|
||
{
|
||
path: 'settings',
|
||
element: <Settings />,
|
||
},
|
||
{
|
||
// Roadmap page (formerly Home/Project Plan)
|
||
path: 'roadmap',
|
||
element: <ProjectPlanPage />,
|
||
},
|
||
{
|
||
path: 'query-builder',
|
||
element: withSuspense(QueryBuilderPage),
|
||
},
|
||
{
|
||
// Redirect old UML viewer route to unified visualize page
|
||
path: 'uml-viewer',
|
||
element: <Navigate to="/visualize" replace />,
|
||
},
|
||
{
|
||
path: 'map',
|
||
element: withSuspense(NDEMapPage),
|
||
},
|
||
{
|
||
path: 'stats',
|
||
element: withSuspense(NDEStatsPage),
|
||
},
|
||
{
|
||
path: 'conversation',
|
||
element: withSuspense(ConversationPage),
|
||
},
|
||
{
|
||
path: 'review',
|
||
element: withSuspense(EntityReviewPage),
|
||
},
|
||
],
|
||
},
|
||
]);
|
||
|
||
function App() {
|
||
const [showUpdateBanner, setShowUpdateBanner] = useState(false);
|
||
|
||
// Preload institution data on app initialization
|
||
// This starts the fetch early so data is ready when user navigates to Map or Browse
|
||
useEffect(() => {
|
||
// Small delay to let the app initialize first
|
||
const timer = setTimeout(() => {
|
||
preloadInstitutions();
|
||
}, 500);
|
||
return () => clearTimeout(timer);
|
||
}, []);
|
||
|
||
// Check for new versions periodically
|
||
useEffect(() => {
|
||
const cleanup = startVersionCheck(() => {
|
||
setShowUpdateBanner(true);
|
||
});
|
||
return cleanup;
|
||
}, []);
|
||
|
||
const handleRefresh = useCallback(() => {
|
||
// Clear caches and reload
|
||
if ('caches' in window) {
|
||
caches.keys().then(names => {
|
||
names.forEach(name => caches.delete(name));
|
||
});
|
||
}
|
||
window.location.reload();
|
||
}, []);
|
||
|
||
return (
|
||
<LanguageProvider>
|
||
<AuthProvider>
|
||
<UIStateProvider>
|
||
{/* New version available banner */}
|
||
{showUpdateBanner && (
|
||
<div className="fixed top-0 left-0 right-0 z-[9999] bg-blue-600 text-white px-4 py-2 text-center text-sm flex items-center justify-center gap-3">
|
||
<span>A new version is available.</span>
|
||
<button
|
||
onClick={handleRefresh}
|
||
className="inline-flex items-center gap-1.5 px-3 py-1 bg-white text-blue-600 rounded font-medium hover:bg-blue-50 transition-colors"
|
||
>
|
||
<RefreshCw className="w-3.5 h-3.5" />
|
||
Refresh
|
||
</button>
|
||
<button
|
||
onClick={() => setShowUpdateBanner(false)}
|
||
className="text-white/80 hover:text-white ml-2"
|
||
aria-label="Dismiss"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
)}
|
||
<RouterProvider router={router} />
|
||
</UIStateProvider>
|
||
</AuthProvider>
|
||
</LanguageProvider>
|
||
);
|
||
}
|
||
|
||
export default App;
|