geocrop-platform./apps/web/src/TechnicalDocs.tsx

328 lines
18 KiB
TypeScript

import React, { useState } from 'react';
interface TechnicalDocsProps {
onBack: () => void;
}
const TechnicalDocs: React.FC<TechnicalDocsProps> = ({ onBack }) => {
const [activeSection, setActiveSection] = useState('architecture');
const sections = [
{ id: 'architecture', label: 'System Architecture' },
{ id: 'infrastructure', label: 'Infrastructure Design' },
{ id: 'mlops', label: 'MLOps Workflow' },
{ id: 'decisions', label: 'Engineering Decisions' },
{ id: 'observability', label: 'Observability' },
{ id: 'live', label: 'Live System Status' },
];
const renderSection = () => {
switch (activeSection) {
case 'architecture':
return (
<div>
<h2 style={{ fontSize: '24px', color: '#1a73e8', marginBottom: '20px' }}>GeoCrop System Architecture</h2>
<div style={{
background: '#f8f9fa',
padding: '30px',
borderRadius: '12px',
border: '1px solid #e9ecef',
marginBottom: '20px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}>
{/* Simplified SVG Architecture Diagram */}
<svg width="600" height="400" viewBox="0 0 600 400">
<rect x="220" y="20" width="160" height="60" rx="8" fill="#e8f0fe" stroke="#1a73e8" strokeWidth="2" />
<text x="300" y="55" textAnchor="middle" fill="#1a73e8" fontWeight="bold" fontSize="14">React Frontend</text>
<path d="M300 80 L300 120" stroke="#5f6368" strokeWidth="2" markerEnd="url(#arrowhead)" />
<rect x="220" y="120" width="160" height="60" rx="8" fill="#fef7e0" stroke="#f9ab00" strokeWidth="2" />
<text x="300" y="155" textAnchor="middle" fill="#b06000" fontWeight="bold" fontSize="14">FastAPI Gateway</text>
<path d="M300 180 L300 220" stroke="#5f6368" strokeWidth="2" markerEnd="url(#arrowhead)" />
<rect x="220" y="220" width="160" height="60" rx="8" fill="#e6ffed" stroke="#28a745" strokeWidth="2" />
<text x="300" y="255" textAnchor="middle" fill="#1e7e34" fontWeight="bold" fontSize="14">ML Worker</text>
<rect x="20" y="220" width="140" height="60" rx="8" fill="#fce8e6" stroke="#d93025" strokeWidth="2" />
<text x="90" y="255" textAnchor="middle" fill="#a50e0e" fontWeight="bold" fontSize="14">Redis Queue</text>
<path d="M220 150 L100 150 L100 220" fill="none" stroke="#5f6368" strokeWidth="2" markerEnd="url(#arrowhead)" />
<path d="M160 250 L220 250" stroke="#5f6368" strokeWidth="2" markerEnd="url(#arrowhead)" />
<rect x="440" y="180" width="140" height="60" rx="8" fill="#f1f3f4" stroke="#5f6368" strokeWidth="2" />
<text x="510" y="215" textAnchor="middle" fill="#3c4043" fontWeight="bold" fontSize="14">MinIO (S3)</text>
<rect x="440" y="260" width="140" height="60" rx="8" fill="#f1f3f4" stroke="#5f6368" strokeWidth="2" />
<text x="510" y="295" textAnchor="middle" fill="#3c4043" fontWeight="bold" fontSize="14">PostGIS DB</text>
<path d="M380 250 L440 210" stroke="#5f6368" strokeWidth="2" markerEnd="url(#arrowhead)" />
<path d="M380 250 L440 290" stroke="#5f6368" strokeWidth="2" markerEnd="url(#arrowhead)" />
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="0" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#5f6368" />
</marker>
</defs>
</svg>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
<div>
<h3 style={{ fontSize: '18px', color: '#202124' }}>Tech Rationale</h3>
<ul style={{ fontSize: '14px', color: '#555' }}>
<li><strong>MinIO:</strong> Local data sovereignty + S3 compatibility for geospatial artifacts.</li>
<li><strong>Redis:</strong> Decouples long-running ML inference from API response times.</li>
<li><strong>TiTiler:</strong> Dynamic COG tiling directly from MinIO without intermediate storage.</li>
</ul>
</div>
<div>
<h3 style={{ fontSize: '18px', color: '#202124' }}>Data Flow</h3>
<p style={{ fontSize: '14px', color: '#555' }}>
Requests flow from the React frontend to the FastAPI gateway, which enqueues jobs in Redis.
The ML Worker pulls STAC data from Digital Earth Africa, runs inference, and persists COGs to MinIO.
</p>
</div>
</div>
</div>
);
case 'infrastructure':
return (
<div>
<h2 style={{ fontSize: '24px', color: '#1a73e8', marginBottom: '20px' }}>Infrastructure Design (K3s)</h2>
<p style={{ marginBottom: '20px' }}>A production-grade Sovereign MLOps cluster designed for low-resource environments.</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
<div style={{ background: '#f8f9fa', padding: '20px', borderRadius: '12px' }}>
<h3 style={{ fontSize: '18px', margin: '0 0 10px' }}>Resource Strategy</h3>
<ul style={{ fontSize: '14px', color: '#555' }}>
<li><strong>Namespaces:</strong> Logical isolation using <code>geocrop</code> and <code>argocd</code>.</li>
<li><strong>API/Web:</strong> Capped at 512MB RAM to maximize efficiency.</li>
<li><strong>Worker/Jupyter:</strong> Allocated up to 2GB for heavy ML compute.</li>
</ul>
</div>
<div style={{ background: '#f8f9fa', padding: '20px', borderRadius: '12px' }}>
<h3 style={{ fontSize: '18px', margin: '0 0 10px' }}>GitOps Layer</h3>
<ul style={{ fontSize: '14px', color: '#555' }}>
<li><strong>Argo CD:</strong> Automated synchronization between Git and the Cluster.</li>
<li><strong>Gitea:</strong> Lightweight self-hosted Git service and CI runner.</li>
<li><strong>Terraform:</strong> Infrastructure as Code for namespaces and volumes.</li>
</ul>
</div>
</div>
<div style={{ marginTop: '20px', padding: '15px', borderLeft: '4px solid #1a73e8', background: '#e8f0fe' }}>
<p style={{ margin: 0, fontSize: '14px' }}><strong>Principle:</strong> "Everything deployed is version-controlled, reproducible, and vendor-agnostic."</p>
</div>
</div>
);
case 'mlops':
return (
<div>
<h2 style={{ fontSize: '24px', color: '#1a73e8', marginBottom: '20px' }}>End-to-End MLOps Workflow</h2>
<div style={{ position: 'relative', paddingLeft: '30px', borderLeft: '2px solid #e8f0fe' }}>
<div style={{ marginBottom: '30px' }}>
<div style={{ position: 'absolute', left: '-11px', width: '20px', height: '20px', borderRadius: '50%', background: '#1a73e8' }}></div>
<h3 style={{ fontSize: '18px', margin: '0 0 5px' }}>1. Data Ingestion & Training</h3>
<p style={{ fontSize: '14px', color: '#555' }}>Zimbabwe crop labels are batched and stored in MinIO. Training is executed in Jupyter or via automated scripts in the cluster.</p>
</div>
<div style={{ marginBottom: '30px' }}>
<div style={{ position: 'absolute', left: '-11px', width: '20px', height: '20px', borderRadius: '50%', background: '#1a73e8' }}></div>
<h3 style={{ fontSize: '18px', margin: '0 0 5px' }}>2. Experiment Tracking (MLflow)</h3>
<p style={{ fontSize: '14px', color: '#555' }}>All runs log parameters, metrics, and models to MLflow at <code>ml.techarvest.co.zw</code>, ensuring full reproducibility.</p>
</div>
<div style={{ marginBottom: '30px' }}>
<div style={{ position: 'absolute', left: '-11px', width: '20px', height: '20px', borderRadius: '50%', background: '#1a73e8' }}></div>
<h3 style={{ fontSize: '18px', margin: '0 0 5px' }}>3. CI/CD & Deployment</h3>
<p style={{ fontSize: '14px', color: '#555' }}>Gitea Actions build the Worker container. Argo CD detects the update and rolls out the new model to the production cluster.</p>
</div>
</div>
</div>
);
case 'decisions':
return (
<div>
<h2 style={{ fontSize: '24px', color: '#1a73e8', marginBottom: '20px' }}>Engineering Decisions & Trade-offs</h2>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
<div style={{ border: '1px solid #eee', padding: '15px', borderRadius: '8px' }}>
<h3 style={{ fontSize: '16px', color: '#d93025' }}>Argo vs Kubeflow</h3>
<p style={{ fontSize: '13px', color: '#555' }}>
<strong>Decision:</strong> Argo CD + Argo Workflows.<br/>
<strong>Rationale:</strong> Kubeflow is too resource-heavy for a single-node VPS. Argo provides the necessary automation with a fraction of the RAM overhead.
</p>
</div>
<div style={{ border: '1px solid #eee', padding: '15px', borderRadius: '8px' }}>
<h3 style={{ fontSize: '16px', color: '#d93025' }}>Gitea vs GitLab</h3>
<p style={{ fontSize: '13px', color: '#555' }}>
<strong>Decision:</strong> Gitea.<br/>
<strong>Rationale:</strong> GitLab requires 4GB+ RAM just to start. Gitea runs comfortably on 256MB, providing high-performance source control and CI for small clusters.
</p>
</div>
<div style={{ border: '1px solid #eee', padding: '15px', borderRadius: '8px' }}>
<h3 style={{ fontSize: '16px', color: '#d93025' }}>Standalone PostGIS</h3>
<p style={{ fontSize: '13px', color: '#555' }}>
<strong>Decision:</strong> Replaced Supabase with native PostGIS container.<br/>
<strong>Rationale:</strong> Removed the GoTrue/PostgREST/Kong overhead while retaining critical spatial query capabilities for geospatial ML.
</p>
</div>
<div style={{ border: '1px solid #eee', padding: '15px', borderRadius: '8px' }}>
<h3 style={{ fontSize: '16px', color: '#d93025' }}>MinIO Storage</h3>
<p style={{ fontSize: '13px', color: '#555' }}>
<strong>Decision:</strong> On-cluster S3-compatible storage.<br/>
<strong>Rationale:</strong> Guarantees data sovereignty and reduces egress costs/latency when training models on large satellite datasets.
</p>
</div>
</div>
</div>
);
case 'observability':
return (
<div>
<h2 style={{ fontSize: '24px', color: '#1a73e8', marginBottom: '20px' }}>Observability & Monitoring</h2>
<div style={{ background: '#f8f9fa', padding: '25px', borderRadius: '12px' }}>
<p style={{ fontSize: '16px', marginBottom: '20px' }}>The platform maintains 99.9% visibility through a layered monitoring stack.</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '15px' }}>
<div style={{ textAlign: 'center', padding: '15px', background: 'white', borderRadius: '8px', boxShadow: '0 2px 5px rgba(0,0,0,0.05)' }}>
<div style={{ fontSize: '24px', marginBottom: '5px' }}>📈</div>
<div style={{ fontWeight: 'bold', fontSize: '14px' }}>Grafana</div>
<div style={{ fontSize: '12px', color: '#666' }}>Metrics Visualization</div>
</div>
<div style={{ textAlign: 'center', padding: '15px', background: 'white', borderRadius: '8px', boxShadow: '0 2px 5px rgba(0,0,0,0.05)' }}>
<div style={{ fontSize: '24px', marginBottom: '5px' }}>🔍</div>
<div style={{ fontWeight: 'bold', fontSize: '14px' }}>Prometheus</div>
<div style={{ fontSize: '12px', color: '#666' }}>Time-series DB</div>
</div>
<div style={{ textAlign: 'center', padding: '15px', background: 'white', borderRadius: '8px', boxShadow: '0 2px 5px rgba(0,0,0,0.05)' }}>
<div style={{ fontSize: '24px', marginBottom: '5px' }}></div>
<div style={{ fontWeight: 'bold', fontSize: '14px' }}>Uptime Kuma</div>
<div style={{ fontSize: '12px', color: '#666' }}>SLA & Heartbeats</div>
</div>
</div>
<div style={{ marginTop: '25px', fontSize: '14px', color: '#5f6368' }}>
<strong>Endpoints:</strong> uptime.techarvest.co.zw | grafana.techarvest.co.zw
</div>
</div>
</div>
);
case 'live':
return (
<div>
<h2 style={{ fontSize: '24px', color: '#1a73e8', marginBottom: '20px' }}>Live Infrastructure Status</h2>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '14px' }}>
<thead>
<tr style={{ borderBottom: '2px solid #eee', textAlign: 'left' }}>
<th style={{ padding: '12px' }}>Service</th>
<th style={{ padding: '12px' }}>Tier</th>
<th style={{ padding: '12px' }}>Status</th>
<th style={{ padding: '12px' }}>Internal Health</th>
</tr>
</thead>
<tbody>
<tr style={{ borderBottom: '1px solid #eee' }}>
<td style={{ padding: '12px' }}><strong>Object Storage (MinIO)</strong></td>
<td style={{ padding: '12px' }}>Core/Data</td>
<td style={{ padding: '12px' }}><span style={{ color: '#28a745', fontWeight: 'bold' }}> ONLINE</span></td>
<td style={{ padding: '12px' }}>Healthy (Erasure Coding)</td>
</tr>
<tr style={{ borderBottom: '1px solid #eee' }}>
<td style={{ padding: '12px' }}><strong>GitOps (Argo CD)</strong></td>
<td style={{ padding: '12px' }}>Ops/Orch</td>
<td style={{ padding: '12px' }}><span style={{ color: '#28a745', fontWeight: 'bold' }}> SYNCED</span></td>
<td style={{ padding: '12px' }}>12 Apps Managed</td>
</tr>
<tr style={{ borderBottom: '1px solid #eee' }}>
<td style={{ padding: '12px' }}><strong>Inference (ML Worker)</strong></td>
<td style={{ padding: '12px' }}>Compute</td>
<td style={{ padding: '12px' }}><span style={{ color: '#28a745', fontWeight: 'bold' }}> READY</span></td>
<td style={{ padding: '12px' }}>Redis-Queue Linked</td>
</tr>
<tr style={{ borderBottom: '1px solid #eee' }}>
<td style={{ padding: '12px' }}><strong>Tracking (MLflow)</strong></td>
<td style={{ padding: '12px' }}>Research</td>
<td style={{ padding: '12px' }}><span style={{ color: '#28a745', fontWeight: 'bold' }}> ACTIVE</span></td>
<td style={{ padding: '12px' }}>PostGIS Backend</td>
</tr>
<tr style={{ borderBottom: '1px solid #eee' }}>
<td style={{ padding: '12px' }}><strong>Lab (JupyterHub)</strong></td>
<td style={{ padding: '12px' }}>Research</td>
<td style={{ padding: '12px' }}><span style={{ color: '#28a745', fontWeight: 'bold' }}> ONLINE</span></td>
<td style={{ padding: '12px' }}>20Gi PV Attached</td>
</tr>
</tbody>
</table>
</div>
</div>
);
default:
return null;
}
};
return (
<div style={{
maxWidth: '1000px',
margin: '40px auto',
padding: '40px',
backgroundColor: 'white',
borderRadius: '16px',
boxShadow: '0 20px 50px rgba(0,0,0,0.15)',
fontFamily: 'system-ui, -apple-system, sans-serif',
color: '#333',
minHeight: '600px'
}}>
<header style={{ borderBottom: '1px solid #eee', paddingBottom: '20px', marginBottom: '30px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h1 style={{ margin: 0, fontSize: '28px', color: '#202124' }}>Technical Portfolio</h1>
<p style={{ margin: '5px 0 0', color: '#5f6368' }}>GeoCrop MLOps Platform Deep-Dive</p>
</div>
<button
onClick={onBack}
style={{
padding: '10px 20px',
backgroundColor: '#f8f9fa',
border: '1px solid #dadce0',
borderRadius: '8px',
cursor: 'pointer',
fontWeight: '600'
}}
>
Back to Profile
</button>
</header>
<div style={{ display: 'flex', gap: '30px' }}>
<nav style={{ width: '250px', flexShrink: 0 }}>
{sections.map(section => (
<button
key={section.id}
onClick={() => setActiveSection(section.id)}
style={{
width: '100%',
textAlign: 'left',
padding: '12px 15px',
marginBottom: '8px',
backgroundColor: activeSection === section.id ? '#e8f0fe' : 'transparent',
color: activeSection === section.id ? '#1a73e8' : '#3c4043',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
fontWeight: activeSection === section.id ? 'bold' : '500',
transition: 'all 0.2s'
}}
>
{section.label}
</button>
))}
</nav>
<main style={{ flex: 1, minWidth: 0 }}>
{renderSection()}
</main>
</div>
</div>
);
};
export default TechnicalDocs;