diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..ea2378d
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,52 @@
+
+
+
+
+ Asteroid Orbit Tracker
+
+
+
+
+
+
+
+
+ Live Orbit Visualization
+
+
+ Blue: Low-risk NEO | Orange: Medium-risk NEO
+
+
+
+
+ Nearby Asteroids
+
+
+ Connection error – using mock data.
+
+
+
+
+ Risk Metrics
+
+
+
+
+
0 km/s
+
Avg Velocity
+
+
+
+
+
+
+
+
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..be84799
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "asteroid-frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "chart.js": "^4.4.4",
+ "react-chartjs-2": "^5.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.1",
+ "autoprefixer": "^10.4.20",
+ "eslint": "^9.6.1",
+ "eslint-plugin-react": "^7.35.0",
+ "eslint-plugin-react-hooks": "^5.0.0",
+ "eslint-plugin-react-refresh": "^0.4.7",
+ "postcss": "^8.4.41",
+ "tailwindcss": "^3.4.10",
+ "vite": "^5.4.1"
+ }
+}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
new file mode 100644
index 0000000..77752ab
--- /dev/null
+++ b/frontend/src/App.jsx
@@ -0,0 +1,22 @@
+import OrbitCanvas from './components/OrbitCanvas.jsx';
+import AsteroidPanel from './components/AsteroidPanel.jsx';
+import Dashboard from './components/Dashboard.jsx';
+
+function App() {
+ return (
+
+
+
+ Asteroid Orbit Tracker 🚀
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/frontend/src/components/AsteroidPanel.js b/frontend/src/components/AsteroidPanel.js
new file mode 100644
index 0000000..6330bfa
--- /dev/null
+++ b/frontend/src/components/AsteroidPanel.js
@@ -0,0 +1,44 @@
+import { getAsteroids } from '../services/api.js';
+
+export async function initAsteroidPanel() {
+ const listEl = document.getElementById('asteroidList');
+ const errorEl = document.getElementById('asteroidError');
+ if (!listEl) return;
+
+ const asteroids = await getAsteroids();
+
+ if (!asteroids || asteroids.length === 0) {
+ if (errorEl) errorEl.classList.remove('hidden');
+ return;
+ }
+
+ listEl.innerHTML = '';
+
+ asteroids.slice(0, 5).forEach((ast) => {
+ const item = document.createElement('div');
+ item.className = 'asteroid-item';
+
+ const name = document.createElement('div');
+ name.className = 'asteroid-name';
+ name.textContent = ast.name;
+
+ const meta1 = document.createElement('div');
+ meta1.className = 'asteroid-meta';
+ meta1.textContent = 'Distance: ' + ast.distance + ' LD';
+
+ const meta2 = document.createElement('div');
+ meta2.className = 'asteroid-meta';
+ meta2.textContent = 'Velocity: ' + ast.velocity + ' km/s';
+
+ const risk = document.createElement('div');
+ risk.className = 'asteroid-risk';
+ risk.textContent = 'Risk: ' + ast.risk;
+
+ item.appendChild(name);
+ item.appendChild(meta1);
+ item.appendChild(meta2);
+ item.appendChild(risk);
+
+ listEl.appendChild(item);
+ });
+}
diff --git a/frontend/src/components/Dashboard.js b/frontend/src/components/Dashboard.js
new file mode 100644
index 0000000..d4a63bc
--- /dev/null
+++ b/frontend/src/components/Dashboard.js
@@ -0,0 +1,18 @@
+import { getStats } from '../services/api.js';
+
+export async function initDashboard() {
+ const totalEl = document.getElementById('metricTotal');
+ const distEl = document.getElementById('metricAvgDistance');
+ const velEl = document.getElementById('metricAvgVelocity');
+
+ if (!totalEl || !distEl || !velEl) return;
+
+ const stats = await getStats();
+ const total = stats.total || 0;
+ const avgDistance = stats.avgDistance || 0;
+ const avgVelocity = stats.avgVelocity || 0;
+
+ totalEl.textContent = total;
+ distEl.textContent = avgDistance.toFixed(1) + ' LD';
+ velEl.textContent = avgVelocity.toFixed(1) + ' km/s';
+}
diff --git a/frontend/src/components/OrbitCanvas.js b/frontend/src/components/OrbitCanvas.js
new file mode 100644
index 0000000..24e579a
--- /dev/null
+++ b/frontend/src/components/OrbitCanvas.js
@@ -0,0 +1,65 @@
+export function initOrbitCanvas() {
+ const canvas = document.getElementById('orbitCanvas');
+ if (!canvas) return;
+
+ const ctx = canvas.getContext('2d');
+
+ function resize() {
+ const rect = canvas.getBoundingClientRect();
+ canvas.width = rect.width * window.devicePixelRatio;
+ canvas.height = rect.height * window.devicePixelRatio;
+ ctx.setTransform(window.devicePixelRatio, 0, 0, window.devicePixelRatio, 0, 0);
+ }
+
+ resize();
+ window.addEventListener('resize', resize);
+
+ let t = 0;
+
+ function draw() {
+ const w = canvas.clientWidth;
+ const h = canvas.clientHeight;
+
+ ctx.fillStyle = 'rgba(2, 6, 23, 0.6)';
+ ctx.fillRect(0, 0, w, h);
+
+ const cx = w / 2;
+ const cy = h / 2;
+
+ ctx.strokeStyle = 'rgba(148, 163, 184, 0.6)';
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ ctx.arc(cx, cy, 80, 0, Math.PI * 2);
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.arc(cx, cy, 140, 0, Math.PI * 2);
+ ctx.stroke();
+
+ ctx.fillStyle = '#22d3ee';
+ ctx.beginPath();
+ ctx.arc(cx, cy, 18, 0, Math.PI * 2);
+ ctx.fill();
+
+ const angle1 = t * 0.02;
+ const x1 = cx + 80 * Math.cos(angle1);
+ const y1 = cy + 80 * Math.sin(angle1);
+ ctx.fillStyle = '#3b82f6';
+ ctx.beginPath();
+ ctx.arc(x1, y1, 6, 0, Math.PI * 2);
+ ctx.fill();
+
+ const angle2 = t * -0.012;
+ const x2 = cx + 140 * Math.cos(angle2);
+ const y2 = cy + 140 * Math.sin(angle2);
+ ctx.fillStyle = '#f97316';
+ ctx.beginPath();
+ ctx.arc(x2, y2, 8, 0, Math.PI * 2);
+ ctx.fill();
+
+ t += 1;
+ requestAnimationFrame(draw);
+ }
+
+ requestAnimationFrame(draw);
+}
diff --git a/frontend/src/main.js b/frontend/src/main.js
new file mode 100644
index 0000000..f6eda32
--- /dev/null
+++ b/frontend/src/main.js
@@ -0,0 +1,9 @@
+import { initOrbitCanvas } from './components/OrbitCanvas.js';
+import { initAsteroidPanel } from './components/AsteroidPanel.js';
+import { initDashboard } from './components/Dashboard.js';
+
+window.addEventListener('DOMContentLoaded', () => {
+ initOrbitCanvas();
+ initAsteroidPanel();
+ initDashboard();
+});
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
new file mode 100644
index 0000000..54b39dd
--- /dev/null
+++ b/frontend/src/main.jsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App.jsx'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+ ,
+)
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
new file mode 100644
index 0000000..d5230f5
--- /dev/null
+++ b/frontend/src/services/api.js
@@ -0,0 +1,33 @@
+const API_BASE = '';
+
+async function fetchJson(path) {
+ if (!API_BASE) throw new Error('API disabled');
+ const res = await fetch(API_BASE + path);
+ if (!res.ok) throw new Error('HTTP ' + res.status);
+ return res.json();
+}
+
+export async function getAsteroids() {
+ try {
+ const data = await fetchJson('/asteroids');
+ return data.asteroids || data || [];
+ } catch (e) {
+ console.warn('Using mock asteroids:', e);
+ return [
+ { id: 1, name: '2026 AB1', distance: 3.4, velocity: 17.1, risk: 'Low' },
+ { id: 2, name: 'Apollo-NEO 33', distance: 1.2, velocity: 22.4, risk: 'Medium' },
+ { id: 3, name: '2025 QX9', distance: 0.8, velocity: 28.9, risk: 'Medium' },
+ { id: 4, name: 'PHA-192', distance: 10.1, velocity: 12.3, risk: 'Low' },
+ { id: 5, name: 'Tempel-NEO', distance: 6.7, velocity: 19.5, risk: 'Low' },
+ ];
+ }
+}
+
+export async function getStats() {
+ try {
+ return await fetchJson('/stats');
+ } catch (e) {
+ console.warn('Using mock stats:', e);
+ return { total: 5, avgDistance: 4.4, avgVelocity: 20.4 };
+ }
+}
diff --git a/frontend/src/services/blank.js b/frontend/src/services/blank.js
new file mode 100644
index 0000000..f70432a
--- /dev/null
+++ b/frontend/src/services/blank.js
@@ -0,0 +1 @@
+//blank
diff --git a/frontend/styles.css b/frontend/styles.css
new file mode 100644
index 0000000..9ba368b
--- /dev/null
+++ b/frontend/styles.css
@@ -0,0 +1,147 @@
+:root {
+ --card: rgba(15, 23, 42, 0.9);
+ --border: rgba(148, 163, 184, 0.4);
+ --text-main: #e5e7eb;
+ --text-muted: #9ca3af;
+ --accent-blue: #3b82f6;
+ --accent-orange: #f97316;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ min-height: 100vh;
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ background: radial-gradient(circle at top, #0f172a 0, #020617 45%, #000 100%);
+ color: var(--text-main);
+ padding: 24px;
+}
+
+.app-header {
+ text-align: center;
+ margin-bottom: 24px;
+}
+
+.app-header h1 {
+ margin: 0;
+ font-size: 2.2rem;
+ background: linear-gradient(90deg, #60a5fa, #a855f7, #22d3ee);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+}
+
+.app-layout {
+ max-width: 1200px;
+ margin: 0 auto;
+ display: grid;
+ grid-template-columns: minmax(0, 2fr) minmax(0, 1.2fr) minmax(0, 1.4fr);
+ gap: 20px;
+}
+
+@media (max-width: 1024px) {
+ .app-layout {
+ grid-template-columns: 1fr;
+ }
+}
+
+.card {
+ background: var(--card);
+ border-radius: 16px;
+ padding: 16px 18px 20px;
+ border: 1px solid var(--border);
+ box-shadow: 0 18px 45px rgba(15, 23, 42, 0.8);
+}
+
+.card h2 {
+ margin: 0 0 12px;
+ font-size: 1.2rem;
+ text-align: center;
+}
+
+.card-orbit {
+ grid-column: span 2;
+}
+
+#orbitCanvas {
+ width: 100%;
+ height: 320px;
+ display: block;
+ border-radius: 12px;
+ background: radial-gradient(circle at top, #020617 0, #000 70%);
+ border: 1px solid rgba(59, 130, 246, 0.5);
+}
+
+.hint {
+ margin-top: 8px;
+ font-size: 0.8rem;
+ text-align: center;
+ color: var(--text-muted);
+}
+
+.asteroid-list {
+ max-height: 320px;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding-right: 6px;
+}
+
+.asteroid-item {
+ padding: 10px 12px;
+ border-radius: 10px;
+ background: rgba(15, 23, 42, 0.85);
+ border-left: 4px solid var(--accent-blue);
+}
+
+.asteroid-name {
+ font-weight: 600;
+ margin-bottom: 4px;
+}
+
+.asteroid-meta {
+ font-size: 0.8rem;
+ color: var(--text-muted);
+}
+
+.metrics-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 10px;
+ margin-top: 10px;
+}
+
+.metric-box {
+ padding: 10px 8px;
+ border-radius: 10px;
+ background: rgba(15, 23, 42, 0.85);
+ text-align: center;
+}
+
+.metric-value {
+ font-size: 1.2rem;
+ font-weight: 700;
+ margin-bottom: 4px;
+}
+
+.metric-label {
+ font-size: 0.8rem;
+ color: var(--text-muted);
+}
+
+.error {
+ margin-top: 10px;
+ font-size: 0.8rem;
+ text-align: center;
+ color: #f97373;
+}
+
+.hidden {
+ display: none;
+}