Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
461 changes: 435 additions & 26 deletions frontend/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@
"cobe": "^0.6.5",
"date-fns": "^4.4.0",
"framer-motion": "^12.40.0",
"globe.gl": "^2.46.1",
"gsap": "^3.15.0",
"lucide-react": "^1.17.0",
"ogl": "^1.0.11",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-hook-form": "^7.77.0",
"react-router-dom": "^7.17.0",
"recharts": "^3.8.1",
"satellite.js": "^4.1.4",
"shadcn": "^4.13.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
Expand Down
48,213 changes: 48,213 additions & 0 deletions frontend/public/datasets/space-track-leo.txt

Large diffs are not rendered by default.

162 changes: 162 additions & 0 deletions frontend/src/components/GlobeBackground.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import React, { useEffect, useRef } from "react";
import Globe from "globe.gl";
import * as satellite from "satellite.js";

const EARTH_RADIUS_KM = 6371; // km
const TIME_STEP = 3 * 1000; // per frame
const radiansToDegrees = (rad: number) => (rad * 180) / Math.PI;

interface SatDataItem {
satrec: satellite.SatRec;
name: string;
lat: number;
lng: number;
alt: number;
}

export const GlobeBackground: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
const globeInstanceRef = useRef<any>(null);
const animationFrameRef = useRef<number | null>(null);

useEffect(() => {
if (!containerRef.current) return;

// Initialize Globe
const world = new Globe(containerRef.current)
.globeImageUrl("//cdn.jsdelivr.net/npm/three-globe/example/img/earth-night.jpg")
.backgroundColor("rgba(0,0,0,0)")
.particleLat("lat")
.particleLng("lng")
.particleAltitude("alt")
.particlesColor(() => "#00e5ff")
.showAtmosphere(true)
.atmosphereColor("#00e5ff")
.atmosphereAltitude(0.25);

// Disable zoom
world.controls().enableZoom = false;

globeInstanceRef.current = world;

// Set initial view
setTimeout(() => {
world.pointOfView({ altitude: 3.0 }, 1000);
}, 200);

// Resize handler
const handleResize = () => {
if (containerRef.current) {
world.width(containerRef.current.clientWidth);
world.height(containerRef.current.clientHeight);
}
};
window.addEventListener("resize", handleResize);
handleResize();

// Fetch and propagate satellites
let isMounted = true;
fetch("/datasets/space-track-leo.txt")
.then((r) => {
if (!r.ok) {
throw new Error(`Failed to fetch dataset: ${r.statusText}`);
}
return r.text();
})
.then((rawData) => {
if (!isMounted) return;

const tleData = rawData
.replace(/\r/g, "")
.split(/\n(?=[^12])/)
.filter((d) => d)
.map((tle) => tle.split("\n"));

const satData: SatDataItem[] = tleData
.map(([name, ...tle]) => {
if (!tle[0] || !tle[1]) return null;
try {
return {
satrec: satellite.twoline2satrec(tle[0], tle[1]),
name: name.trim().replace(/^0 /, ""),
lat: 0,
lng: 0,
alt: 0,
};
} catch (e) {
return null;
}
})
.filter((d): d is SatDataItem => {
if (!d) return false;
const pos = satellite.propagate(d.satrec, new Date());
return !!(pos && typeof pos === "object" && pos.position);
});

// Time ticker loop
let time = new Date();

const frameTicker = () => {
if (!isMounted) return;

time = new Date(+time + TIME_STEP);

// Update satellite positions
const gmst = satellite.gstime(time);
satData.forEach((d) => {
const eci = satellite.propagate(d.satrec, time);
if (eci && typeof eci === "object" && eci.position) {
const position = eci.position as satellite.PositionAndVelocity["position"];
if (typeof position === "object" && "x" in position) {
const gdPos = satellite.eciToGeodetic(position, gmst);
d.lat = radiansToDegrees(gdPos.latitude);
d.lng = radiansToDegrees(gdPos.longitude);
d.alt = gdPos.height / EARTH_RADIUS_KM;
}
} else {
d.lat = NaN;
d.lng = NaN;
d.alt = NaN;
}
});

const validSats = satData.filter(
(d) => !isNaN(d.lat) && !isNaN(d.lng) && !isNaN(d.alt)
);
world.particlesData([validSats]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The particlesData call wraps validSats in an extra array, so globe.gl receives a single-element array whose sole item is the whole SatDataItem[] array. When globe.gl iterates the data and tries to read d["lat"] / d["lng"] / d["alt"] on that array, it gets undefined for all fields, so no satellite particles will ever be rendered.

Suggested change
world.particlesData([validSats]);
world.particlesData(validSats);
Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/components/GlobeBackground.tsx
Line: 126

Comment:
The `particlesData` call wraps `validSats` in an extra array, so globe.gl receives a single-element array whose sole item is the whole `SatDataItem[]` array. When globe.gl iterates the data and tries to read `d["lat"]` / `d["lng"]` / `d["alt"]` on that array, it gets `undefined` for all fields, so no satellite particles will ever be rendered.

```suggestion
          world.particlesData(validSats);
```

How can I resolve this? If you propose a fix, please make it concise.


// Rotate globe slowly
const currentPov = world.pointOfView();
world.pointOfView({
lng: currentPov.lng + 0.05,
});

animationFrameRef.current = requestAnimationFrame(frameTicker);
};
Comment on lines +96 to +135

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
awk 'substr($0, 1, 2) == "1 " { count++ } END { print count }' \
  frontend/public/datasets/space-track-leo.txt

Repository: 7-Blocks/Kepler

Length of output: 159


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- GlobeBackground.tsx (relevant range) ---'
nl -ba frontend/src/components/GlobeBackground.tsx | sed -n '1,220p'

echo
echo '--- satellite dataset line count / sample shape ---'
python3 - <<'PY'
from pathlib import Path
p = Path('frontend/public/datasets/space-track-leo.txt')
count = 0
sample = []
with p.open() as f:
    for i, line in enumerate(f, 1):
        if i <= 6:
            sample.append(line.rstrip('\n'))
        if line.startswith('1 '):
            count += 1
print('line_count_starting_with_1_space =', count)
print('first_lines:')
for s in sample:
    print(repr(s))
PY

Repository: 7-Blocks/Kepler

Length of output: 238


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the satellite data loader and animation setup in the component.
ast-grep outline frontend/src/components/GlobeBackground.tsx --view expanded

Repository: 7-Blocks/Kepler

Length of output: 496


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the surrounding effect setup to understand how often this runs.
sed -n '1,260p' frontend/src/components/GlobeBackground.tsx | nl -ba | sed -n '1,260p'

Repository: 7-Blocks/Kepler

Length of output: 193


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the component around the animation loop only.
nl -ba frontend/src/components/GlobeBackground.tsx | sed -n '80,160p'

Repository: 7-Blocks/Kepler

Length of output: 193


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- GlobeBackground.tsx around the animation loop ---'
sed -n '1,220p' frontend/src/components/GlobeBackground.tsx | sed -n '1,220p' | awk '{printf "%4d  %s\n", NR, $0}' | sed -n '70,170p'

echo
echo '--- Find the dataset loader / fetch path ---'
rg -n "space-track-leo|particlesData|requestAnimationFrame|TIME_STEP|pointOfView|SatDataItem" frontend/src/components/GlobeBackground.tsx frontend/src -S

Repository: 7-Blocks/Kepler

Length of output: 6131


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('frontend/src/components/GlobeBackground.tsx')
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 80 <= i <= 170:
        print(f"{i:4d}  {line}")
PY

Repository: 7-Blocks/Kepler

Length of output: 3227


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('frontend/src/components/GlobeBackground.tsx')
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 1 <= i <= 220:
        if 'particlesData' in line or 'requestAnimationFrame' in line or 'TIME_STEP' in line or 'pointOfView' in line or 'space-track-leo' in line:
            print(f"{i:4d}  {line}")
PY

Repository: 7-Blocks/Kepler

Length of output: 579


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('frontend/public/datasets/space-track-leo.txt')
count = 0
valid = 0
with p.open() as f:
    for line in f:
        if line.startswith('1 '):
            count += 1
        if line.startswith('2 '):
            valid += 1
print('line_count_starting_with_1_space =', count)
print('line_count_starting_with_2_space =', valid)
PY

Repository: 7-Blocks/Kepler

Length of output: 235


Throttle globe updates to elapsed time. This propagates the full 16,071-satellite catalog and calls particlesData on every RAF, so the work scales with refresh rate and the animation runs faster on high-Hz displays. Use RAF delta time for rotation and throttle satellite propagation/data uploads to a fixed interval.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/GlobeBackground.tsx` around lines 96 - 135, Update
the frameTicker loop to accept RAF timestamps and calculate elapsed time, using
that delta to scale globe rotation consistently across refresh rates. Accumulate
elapsed time and only propagate the satellite catalog and call
world.particlesData when the fixed update interval has elapsed, while continuing
to schedule RAF frames and preserving the existing validity filtering.


frameTicker();
})
.catch((err) => {
console.error("Error loading or running globe satellites:", err);
});

return () => {
isMounted = false;
window.removeEventListener("resize", handleResize);
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
if (containerRef.current) {
containerRef.current.innerHTML = "";
}
};
}, []);

return (
<div className="absolute inset-0 w-full h-full overflow-hidden select-none">
<div ref={containerRef} className="w-full h-full" />
</div>
);
};

export default GlobeBackground;
15 changes: 6 additions & 9 deletions frontend/src/components/layouts/MarketingLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@ function MarketingNavBar() {
}, [location.pathname, location.hash]);

const linkClass = (active?: boolean) =>
`font-body-ui text-[13px] sm:text-sm transition-colors duration-150 no-underline px-1 ${
active ? "text-white" : "text-white/65 hover:text-white"
`font-body-ui text-[13px] sm:text-sm transition-colors duration-150 no-underline px-1 ${active ? "text-white" : "text-white/65 hover:text-white"
}`;

return (
Expand Down Expand Up @@ -123,14 +122,12 @@ function MarketingNavBar() {
<span className="sr-only">Menu</span>
<span className="flex flex-col gap-1.5" aria-hidden="true">
<span
className={`block h-px w-3.5 bg-white transition-transform ${
menuOpen ? "translate-y-[3.5px] rotate-45" : ""
}`}
className={`block h-px w-3.5 bg-white transition-transform ${menuOpen ? "translate-y-[3.5px] rotate-45" : ""
}`}
/>
<span
className={`block h-px w-3.5 bg-white transition-transform ${
menuOpen ? "-translate-y-[3.5px] -rotate-45" : ""
}`}
className={`block h-px w-3.5 bg-white transition-transform ${menuOpen ? "-translate-y-[3.5px] -rotate-45" : ""
}`}
/>
</span>
</button>
Expand Down Expand Up @@ -283,7 +280,7 @@ function MarketingFooter() {

<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pt-6 section-rule section-rule-full">
<p className="font-technical-data text-[11px] text-[#8892A6] m-0">
© {new Date().getFullYear()} Kepler. Orbital intelligence platform.
© {new Date().getFullYear()} 7Blocks. We Make Software That Solves Pain Points
</p>
<p className="font-technical-data text-[11px] text-[#8892A6]/70 m-0">
Built for the crowded sky.
Expand Down
Loading
Loading