328 lines
18 KiB
TypeScript
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;
|