From 38438f410c1e8ec8b7aab7a329c9960c10af94c0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:19:43 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20MobyDB=20+=20latlng=20spatial=20engine?= =?UTF-8?q?=20integration=20=E2=80=94=20provenance,=20tracking,=20geofenci?= =?UTF-8?q?ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MobyDB (geospatial-native database): - HTTP client for MobyDB's axum API (spacetime address model: H3 cell + epoch + Ed25519 key) - Provenance records with PostgreSQL fallback/mirror - Supply chain step tracking with Merkle proof verification - Epoch management (seal, verify, query) - 10 tRPC endpoints (record, query, seal, prove, verify, supply chain) - Drizzle schema: provenance_records, provenance_epochs, provenance_keys, supply_chain_steps latlng (real-time geospatial object engine): - HTTP client for latlng server (collections, objects, geofences, events) - Fleet/vehicle tracking, cold chain monitoring, distributor coverage - Geofence zone management with webhook delivery - Tracking history with position logging - 12 tRPC endpoints (update, nearby, list, history, geofence CRUD, events) - Drizzle schema: tracked_objects, tracking_history, geofence_zones, geofence_events PWA pages (4 new): - Provenance Explorer: spacetime address model, epoch browsing, Merkle proofs - Fleet Tracker: real-time object map, collection views, KPIs - Geofence Manager: zone CRUD, event monitoring, webhook config - Supply Chain Provenance: verifiable farm-to-table tracking timeline Navigation: CategoryHub + DashboardLayout sidebar entries under Spatial & Weather Build: tsc 0 errors, 586 tests pass Co-Authored-By: Patrick Munis --- client/src/App.tsx | 9 + client/src/components/CategoryHub.tsx | 7 +- client/src/components/DashboardLayout.tsx | 10 +- client/src/pages/FleetTracker.tsx | 272 +++++++++++ client/src/pages/GeofenceManager.tsx | 323 ++++++++++++ client/src/pages/ProvenanceExplorer.tsx | 374 ++++++++++++++ client/src/pages/SupplyChainProvenance.tsx | 257 ++++++++++ drizzle/schema-spatial-engines.ts | 164 +++++++ server/lib/latlng-client.ts | 425 ++++++++++++++++ server/lib/mobydb-client.ts | 288 +++++++++++ server/routers/latlng-router.ts | 512 +++++++++++++++++++ server/routers/mobydb-router.ts | 543 +++++++++++++++++++++ server/trpc.ts | 4 + 13 files changed, 3186 insertions(+), 2 deletions(-) create mode 100644 client/src/pages/FleetTracker.tsx create mode 100644 client/src/pages/GeofenceManager.tsx create mode 100644 client/src/pages/ProvenanceExplorer.tsx create mode 100644 client/src/pages/SupplyChainProvenance.tsx create mode 100644 drizzle/schema-spatial-engines.ts create mode 100644 server/lib/latlng-client.ts create mode 100644 server/lib/mobydb-client.ts create mode 100644 server/routers/latlng-router.ts create mode 100644 server/routers/mobydb-router.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index 17be333..c7c9cf8 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -139,6 +139,10 @@ const SpatialSpectralIndex = lazy(() => import("./pages/SpatialSpectralIndex")); const SpatialH3Analysis = lazy(() => import("./pages/SpatialH3Analysis")); const SpatialVectorAnalysis = lazy(() => import("./pages/SpatialVectorAnalysis")); const SpatialTileCache = lazy(() => import("./pages/SpatialTileCache")); +const ProvenanceExplorer = lazy(() => import("./pages/ProvenanceExplorer")); +const FleetTracker = lazy(() => import("./pages/FleetTracker")); +const GeofenceManager = lazy(() => import("./pages/GeofenceManager")); +const SupplyChainProvenance = lazy(() => import("./pages/SupplyChainProvenance")); const ColdChainMonitoring = lazy(() => import("./pages/ColdChainMonitoring")); const PriceAlertsDashboard = lazy(() => import("./pages/PriceAlertsDashboard")); const SubscriptionBoxes = lazy(() => import("./pages/SubscriptionBoxes")); @@ -307,6 +311,11 @@ function Router() { + {/* === MobyDB + latlng Spatial Engine Pages === */} + + + + diff --git a/client/src/components/CategoryHub.tsx b/client/src/components/CategoryHub.tsx index f9ee85c..e3857b3 100644 --- a/client/src/components/CategoryHub.tsx +++ b/client/src/components/CategoryHub.tsx @@ -11,7 +11,8 @@ import { Activity, Droplets, Fish, AlertTriangle, Store, RotateCcw, Apple, ChevronDown, WifiOff, CheckCircle, Mail, Workflow, PieChart, GanttChart, Archive, Warehouse, Scale, Landmark, - Milk, Beef, Heart, Baby, Syringe, Factory + Milk, Beef, Heart, Baby, Syringe, Factory, + Fingerprint, Navigation, Radio, Fence, Link2, Key } from "lucide-react"; interface FeatureCard { @@ -102,6 +103,10 @@ const categoryFeatures: Record = { { href: "/spatial-h3-analysis", label: "H3 Grid", icon: Target, description: "Hex coverage analysis", badge: "NEW" }, { href: "/spatial-vector-analysis", label: "Vector Analysis", icon: Leaf, description: "Buffer, clip, dissolve", badge: "NEW" }, { href: "/spatial-tile-cache", label: "Offline Tiles", icon: WifiOff, description: "Cache for offline", badge: "NEW" }, + { href: "/provenance-explorer", label: "Provenance Explorer", icon: Fingerprint, description: "MobyDB spacetime proofs", badge: "NEW" }, + { href: "/fleet-tracker", label: "Fleet Tracker", icon: Navigation, description: "latlng real-time tracking", badge: "NEW" }, + { href: "/geofence-manager", label: "Geofence Manager", icon: Fence, description: "Zone monitoring & alerts", badge: "NEW" }, + { href: "/supply-chain-provenance", label: "Supply Chain", icon: Link2, description: "Verifiable farm-to-table", badge: "NEW" }, ], }, { diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx index 61b7520..6a83fd5 100644 --- a/client/src/components/DashboardLayout.tsx +++ b/client/src/components/DashboardLayout.tsx @@ -70,7 +70,11 @@ import { ArrowRightLeft, Warehouse, Fish, - Droplets + Droplets, + Fingerprint, + Navigation, + Fence, + Link2 } from "lucide-react"; // Navigation sections organized by domain @@ -182,6 +186,10 @@ const navSections = [ { href: "/weather", label: "Weather Dashboard", icon: Cloud }, { href: "/weather-alerts", label: "Weather Alerts", icon: AlertTriangle }, { href: "/precision-agriculture", label: "Precision Agriculture", icon: Target }, + { href: "/provenance-explorer", label: "Provenance Explorer", icon: Fingerprint }, + { href: "/fleet-tracker", label: "Fleet Tracker", icon: Navigation }, + { href: "/geofence-manager", label: "Geofence Manager", icon: Fence }, + { href: "/supply-chain-provenance", label: "Supply Chain Provenance", icon: Link2 }, ] }, { diff --git a/client/src/pages/FleetTracker.tsx b/client/src/pages/FleetTracker.tsx new file mode 100644 index 0000000..1cce84e --- /dev/null +++ b/client/src/pages/FleetTracker.tsx @@ -0,0 +1,272 @@ +/** + * Fleet Tracker โ€” latlng Real-Time Object Tracking Dashboard + * + * Live map of tracked vehicles, cold chain units, distributors, and sensors. + * Powered by latlng geospatial object engine with PostgreSQL fallback. + */ +import { useState } from "react"; +import DashboardLayout from "@/components/DashboardLayout"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { trpc } from "@/lib/trpc"; +import { + Truck, MapPin, Thermometer, Radio, Activity, Navigation, + Battery, Wifi, Clock, AlertTriangle, Eye, BarChart3, + Satellite, Gauge, RefreshCw, +} from "lucide-react"; + +type FleetTab = "map" | "fleet" | "cold_chain" | "distributors" | "sensors"; + +const COLLECTIONS = [ + { id: "fleet" as const, label: "Fleet Vehicles", icon: Truck, color: "text-blue-600" }, + { id: "cold_chain" as const, label: "Cold Chain", icon: Thermometer, color: "text-cyan-600" }, + { id: "distributors" as const, label: "Distributors", icon: MapPin, color: "text-purple-600" }, + { id: "farmers" as const, label: "Farmers", icon: Navigation, color: "text-green-600" }, + { id: "sensors" as const, label: "IoT Sensors", icon: Radio, color: "text-orange-600" }, +] as const; + +export default function FleetTracker() { + const [activeTab, setActiveTab] = useState("map"); + const [selectedCollection, setSelectedCollection] = useState<"fleet" | "cold_chain" | "distributors" | "farmers" | "sensors">("fleet"); + + const status = trpc.latlng.status.useQuery(); + const stats = trpc.latlng.dashboardStats.useQuery(); + const objects = trpc.latlng.listObjects.useQuery({ + collection: selectedCollection, + limit: 100, + }); + + const tabs = [ + { id: "map" as const, label: "Live Map", icon: MapPin }, + { id: "fleet" as const, label: "Fleet", icon: Truck }, + { id: "cold_chain" as const, label: "Cold Chain", icon: Thermometer }, + { id: "distributors" as const, label: "Distributors", icon: Navigation }, + { id: "sensors" as const, label: "Sensors", icon: Radio }, + ]; + + return ( + +
+ {/* Hero Header */} +
+
+
+ +
+
+

Fleet Tracker

+

+ latlng Real-Time Tracking ยท Geospatial Object Engine ยท Live Positions +

+
+
+
+ + {status.data?.latlngConnected ? "๐ŸŸข latlng Connected" : "๐Ÿ”ด latlng Offline โ€” PostgreSQL Fallback"} + + + {stats.data?.totalTracked ?? 0} Tracked Objects + + + {stats.data?.activeLastHour ?? 0} Active (1h) + +
+
+ + {/* Tab Navigation */} +
+ {tabs.map((tab) => ( + + ))} +
+ + {/* KPI Cards */} +
+ {COLLECTIONS.map((coll) => ( + + setSelectedCollection(coll.id)}> + +
+ {stats.data?.byCollection?.[coll.id] ?? 0} +
+
{coll.label}
+
+
+ ))} +
+ + {/* Map Tab */} + {activeTab === "map" && ( + + + + + + Live Tracking Map + + + + + +
+
+ {objects.data && objects.data.length > 0 ? ( +
+ {objects.data.map((obj) => ( +
+
+
+ {obj.label || obj.id} +
+
+ {obj.latitude.toFixed(4)}, {obj.longitude.toFixed(4)} +
+ {obj.speed !== null && obj.speed !== undefined && ( +
+ + {obj.speed} km/h +
+ )} +
+ ))} +
+ ) : ( +
+ +

No Objects Tracked Yet

+

+ Use the API to start tracking fleet vehicles, cold chain units, and sensors. +

+
+ )} +
+ + + )} + + {/* Collection List Views */} + {activeTab !== "map" && ( + + + + + {COLLECTIONS.find(c => c.id === selectedCollection)?.icon && + (() => { const Icon = COLLECTIONS.find(c => c.id === selectedCollection)!.icon; return ; })() + } + {COLLECTIONS.find(c => c.id === selectedCollection)?.label} + + {objects.data?.length ?? 0} objects + + + + {objects.data && objects.data.length > 0 ? ( +
+ {objects.data.map((obj) => ( +
+
+
+
+ {obj.label || obj.id} +
+ {obj.latitude.toFixed(4)}, {obj.longitude.toFixed(4)} + {obj.speed !== null && obj.speed !== undefined ? ` ยท ${obj.speed} km/h` : ""} +
+
+
+
+ {(() => { + const f = obj.fields as Record | null; + if (!f || typeof f !== "object") return null; + return ( + <> + {"temperature" in f && ( + + + {String(f.temperature)}ยฐC + + )} + {"battery" in f && ( + + + {String(f.battery)}% + + )} + + ); + })()} + + + {new Date(obj.lastUpdated).toLocaleTimeString()} + +
+
+ ))} +
+ ) : ( +
+ +

No objects in this collection.

+
+ )} + + + )} + + {/* System Stats */} +
+ + +
+ + Geofence Zones +
+
{stats.data?.activeGeofences ?? 0}
+
Active geofence monitoring zones
+
+
+ + +
+ + Geofence Events +
+
{stats.data?.totalEvents ?? 0}
+
Enter/exit/dwell events recorded
+
+
+ + +
+ + History Points +
+
{(stats.data?.historyPoints ?? 0).toLocaleString()}
+
Position history data points
+
+
+
+
+ + ); +} diff --git a/client/src/pages/GeofenceManager.tsx b/client/src/pages/GeofenceManager.tsx new file mode 100644 index 0000000..4e9022d --- /dev/null +++ b/client/src/pages/GeofenceManager.tsx @@ -0,0 +1,323 @@ +/** + * Geofence Manager โ€” latlng Geofence Zone & Event Management + * + * Create, monitor, and manage geofence zones for farm boundaries, + * delivery zones, cold chain routes, and distributor coverage areas. + */ +import { useState } from "react"; +import DashboardLayout from "@/components/DashboardLayout"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { trpc } from "@/lib/trpc"; +import { + Shield, MapPin, Hexagon, AlertTriangle, Bell, Fence, + Plus, Trash2, Eye, Activity, Clock, ArrowUpRight, + ArrowDownLeft, Radio, Target, Zap, +} from "lucide-react"; + +type GeoTab = "zones" | "events" | "create"; + +const ZONE_TYPES = [ + { id: "farm_boundary", label: "Farm Boundary", icon: Hexagon, color: "bg-green-100 text-green-700" }, + { id: "delivery_zone", label: "Delivery Zone", icon: MapPin, color: "bg-blue-100 text-blue-700" }, + { id: "cold_chain_route", label: "Cold Chain Route", icon: Target, color: "bg-cyan-100 text-cyan-700" }, + { id: "coverage_area", label: "Coverage Area", icon: Radio, color: "bg-purple-100 text-purple-700" }, + { id: "warehouse", label: "Warehouse", icon: Fence, color: "bg-orange-100 text-orange-700" }, + { id: "exclusion_zone", label: "Exclusion Zone", icon: Shield, color: "bg-red-100 text-red-700" }, +]; + +const EVENT_ICONS: Record = { + enter: ArrowUpRight, + exit: ArrowDownLeft, + inside: Eye, + outside: Radio, + cross: Zap, +}; + +export default function GeofenceManager() { + const [activeTab, setActiveTab] = useState("zones"); + const [filterType, setFilterType] = useState(""); + + const zones = trpc.latlng.listGeofences.useQuery({ + zoneType: filterType || undefined, + }); + const events = trpc.latlng.getGeofenceEvents.useQuery({ limit: 50 }); + const stats = trpc.latlng.dashboardStats.useQuery(); + + const tabs = [ + { id: "zones" as const, label: "Zones", icon: Hexagon }, + { id: "events" as const, label: "Events", icon: Bell }, + { id: "create" as const, label: "Create Zone", icon: Plus }, + ]; + + return ( + +
+ {/* Hero Header */} +
+
+
+ +
+
+

Geofence Manager

+

+ latlng Geofencing ยท Webhook Events ยท Zone Monitoring ยท NODWELL Detection +

+
+
+
+ + {stats.data?.activeGeofences ?? 0} Active Zones + + + {stats.data?.totalEvents ?? 0} Events Recorded + +
+
+ + {/* Tab Navigation */} +
+ {tabs.map((tab) => ( + + ))} +
+ + {/* Zones Tab */} + {activeTab === "zones" && ( +
+ {/* Zone Type Filter */} +
+ + {ZONE_TYPES.map((zt) => ( + + ))} +
+ + {/* Zone List */} + + + + + + Geofence Zones + + {zones.data?.length ?? 0} zones + + + + {zones.data && zones.data.length > 0 ? ( +
+ {zones.data.map((zone) => { + const zt = ZONE_TYPES.find(z => z.id === zone.zoneType); + return ( +
+
+
+ {zt?.icon && } +
+
+ {zone.name} +
+ {zone.collection} ยท {(zone.detectEvents as string[])?.join(", ") || "enter, exit"} +
+
+
+
+ + {zone.latlngSynced ? "Synced" : "Local"} + + + {zone.active ? "Active" : "Inactive"} + +
+
+ ); + })} +
+ ) : ( +
+ +

No geofence zones configured yet.

+
+ )} +
+
+
+ )} + + {/* Events Tab */} + {activeTab === "events" && ( + + + + + Geofence Events + + + + {events.data && events.data.length > 0 ? ( +
+ {events.data.map((event) => { + const EventIcon = EVENT_ICONS[event.eventType] || Activity; + const isEntry = event.eventType === "enter"; + const isExit = event.eventType === "exit"; + return ( +
+
+
+ +
+
+ {event.objectId} + + {event.eventType} โ†’ {event.zoneName} + +
+ {event.collection} + {event.latitude && ` ยท ${event.latitude.toFixed(4)}, ${event.longitude?.toFixed(4)}`} +
+
+
+
+ + {event.webhookDelivered ? "Delivered" : "Pending"} + + + + {new Date(event.occurredAt).toLocaleTimeString()} + +
+
+ ); + })} +
+ ) : ( +
+ +

No geofence events recorded yet.

+
+ )} +
+
+ )} + + {/* Create Zone Tab */} + {activeTab === "create" && ( +
+ + + + + Create Geofence Zone + + + +
+
+ + +
+
+ +
+ {ZONE_TYPES.map((zt) => ( + + ))} +
+
+
+ +
+ +
+ {["enter", "exit", "inside", "outside", "cross"].map((evt) => ( + + {evt} + + ))} +
+
+ +
+ + +
+ +
+ +
+

+ Draw the geofence boundary on the map, or paste GeoJSON Polygon/MultiPolygon geometry. +

+