Skip to content

Feature/cinematic camera - #176

Open
SohammPawarr wants to merge 3 commits into
7-Blocks:mainfrom
SohammPawarr:feature/cinematic-camera
Open

Feature/cinematic camera#176
SohammPawarr wants to merge 3 commits into
7-Blocks:mainfrom
SohammPawarr:feature/cinematic-camera

Conversation

@SohammPawarr

@SohammPawarr SohammPawarr commented Aug 8, 2026

Copy link
Copy Markdown

User description

Description

Implemented two major visual and real-time tracking features for the Kepler globe:

  1. Real-Time Flyby Notification System: Built a Mission Control-style alert system that monitors and notifies users of upcoming satellite flybys over bookmarked locations based on orbital predictions (ETA, elevation, velocity).
  2. Cinematic Orbit Camera Mode: Overhauled satellite tracking by dynamically animating the selected satellite's real-time position on every frame. Introduced smooth interpolation and 5 camera perspectives (Free, Chase, Cockpit, Earth Observer, and Orbital) with a sleek UI control panel.

Related Issue : #170

Testing Details

  • Verified interval polling logic for the flyby background engine.
  • Ensured unselected satellites remain static to preserve rendering performance while tracking is active.
  • Tested Cesium.Cartesian3.lerp smoothing logic for camera view transitions.
  • Type-checked the entire frontend via Vite/TSC.

CodeAnt-AI Description

Add satellite flyby alerts and cinematic tracking cameras

What Changed

  • Monitors selected satellites for upcoming visible passes over the user's location and saved locations, then shows alerts with ETA, altitude, speed, and peak elevation
  • Adds sound and browser notifications, configurable warning windows, alert dismissal, history, and a clear-all option
  • Lets users open an alert to track the satellite live on the globe
  • Adds Free, Chase, Cockpit, Earth Observer, and Orbital camera views that smoothly follow a selected satellite in real time
  • Replaces the unused schedule control with a flyby alert history and settings panel

Impact

✅ Earlier notice of visible satellite passes
✅ Faster access to live satellite tracking
✅ Smoother cinematic globe views

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Summary by CodeRabbit

  • New Features
    • Added cinematic satellite camera modes, including Free, Chase, Cockpit, Earth Observer, and Orbital views.
    • Added automatic flyby alerts with optional sound, browser notifications, warning-window settings, dismissal, and live tracking.
    • Added flyby notification history with controls to review, clear, and manage alert preferences.
    • Added satellite selection, spotlighting, and smoother real-time orbital tracking.
    • Added improved satellite pass calculations for location, distance, altitude, and elevation.
  • Chores
    • Updated project configuration to better support Python tooling and current TypeScript settings.

Copilot AI lite review requested due to automatic review settings August 8, 2026 10:11
@codeant-ai

codeant-ai Bot commented Aug 8, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Reviewed your PR 5ee1046 Aug 08, 2026 · 10:11 10:14

@codeant-ai

codeant-ai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@github-actions github-actions Bot added backend Backend development database Database and schema related changes enhancement New feature or request frontend Frontend development size/XL Very large or complex contribution. testing Tests added or improved type:backend Changes backend services or server-side logic. type:feature Introduces a new feature or enhancement. type:frontend Changes frontend or client-side code. type:testing Adds or updates automated tests. labels Aug 8, 2026
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Satellite experience

Layer / File(s) Summary
Orbital calculations and cinematic camera
frontend/src/utils/orbitCalc.ts, frontend/src/store/uiStore.ts, frontend/src/hooks/useCinematicCamera.ts, frontend/src/components/EarthTwin.tsx, frontend/src/components/ui/CameraControls.tsx
Adds shared orbital calculations, camera mode state, Cesium camera tracking, and five camera controls.
Flyby detection and notification state
frontend/src/hooks/useFlybyEngine.ts, frontend/src/store/notificationStore.ts
Evaluates satellite passes for current and bookmarked locations, then creates deduplicated notifications on a 30-second schedule.
Notification and layout integration
frontend/src/components/ui/FlybyNotification.tsx, frontend/src/components/ui/NotificationCenter.tsx, frontend/src/components/layouts/MainLayout.tsx
Adds animated flyby alerts, notification history and preferences, live tracking, and layout integration for notification and camera controls.

Repository configuration

Layer / File(s) Summary
Repository and compiler configuration
.gitignore, frontend/tsconfig.app.json
Adds Python-related ignore rules and removes the deprecated ignoreDeprecations compiler option.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CameraControls
  participant useUIStore
  participant useCinematicCamera
  participant CesiumViewer
  User->>CameraControls: select camera mode
  CameraControls->>useUIStore: setCameraMode(mode)
  useCinematicCamera->>useUIStore: read selected satellite and mode
  useCinematicCamera->>CesiumViewer: update camera on preRender
Loading
sequenceDiagram
  participant useFlybyEngine
  participant SatelliteCatalog
  participant orbitCalc
  participant useNotificationStore
  participant NotificationCenter
  useFlybyEngine->>SatelliteCatalog: fetch selected satellite data
  useFlybyEngine->>orbitCalc: propagate positions and calculate elevation
  useFlybyEngine->>useNotificationStore: create qualifying notification
  NotificationCenter->>useNotificationStore: read active notifications
Loading

Possibly related issues

Possibly related PRs

  • 7-Blocks/Kepler#175 — Shares the flyby notification components, hooks, stores, orbital utilities, and UI integrations.
  • 7-Blocks/Kepler#142 — Shares satellite selection, spotlight integration, and orbital conversion changes in EarthTwin.tsx.
  • 7-Blocks/Kepler#148 — Shares spotlight behavior and the keplerToLatLonAlt utility.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides a summary, issue reference, and testing details, but it omits several required template sections and checklist selections. Add the Type of Change, Screenshots, Breaking Changes, Checklist, and ECSoC26 sections, and complete the applicable testing checkboxes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title identifies the cinematic camera feature, which is a major part of the PR, but it omits the flyby notification feature.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Warning

⚠️ This pull request shows signs of AI-generated slop (ai_padded_prose). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@codeant-ai codeant-ai Bot added the size:XL This PR changes 500-999 lines, ignoring generated files label Aug 8, 2026
@github-actions github-actions Bot added AI Artificial Intelligence and Machine Learning documentation Improvements or additions to documentation type:documentation Improves project documentation. labels Aug 8, 2026
Comment on lines +355 to +360
useSpotlightEffect({
viewer: viewerInstance,
selectedId: selectedSatelliteId,
entitiesRef,
hoveredId: hoveredObject?.catalog_number ?? null,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The new call does not satisfy useSpotlightEffect's required argument contract: it omits catalogMapRef, collisionSetRef, and datasetVersion. This causes the TypeScript build to fail and prevents the spotlight effect from being initialized. [api mismatch]

Severity Level: Critical 🚨
- ❌ Frontend TypeScript build fails during compilation.
- ❌ EarthTwin globe and cinematic features cannot be deployed.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** frontend/src/components/EarthTwin.tsx
**Line:** 355:360
**Comment:**
	*Api Mismatch: The new call does not satisfy `useSpotlightEffect`'s required argument contract: it omits `catalogMapRef`, `collisionSetRef`, and `datasetVersion`. This causes the TypeScript build to fail and prevents the spotlight effect from being initialized.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

const playBeep = () => {
try {
const AudioContext = window.AudioContext || (window as any).webkitAudioContext;
if (!AudioContext) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Every notification creates a new AudioContext, but the context is never closed after the oscillator finishes. Repeated alerts accumulate audio resources and can eventually exhaust or pressure browser audio resources. [resource leak]

Severity Level: Major ⚠️
- ⚠️ Long-lived sessions retain completed audio resources.
- ⚠️ Repeated flyby alerts can increase browser audio-memory pressure.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** frontend/src/components/ui/FlybyNotification.tsx
**Line:** 16:16
**Comment:**
	*Resource Leak: Every notification creates a new `AudioContext`, but the context is never closed after the oscillator finishes. Repeated alerts accumulate audio resources and can eventually exhaust or pressure browser audio resources.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +61 to +64
const handleTrack = () => {
setSelectedSatelliteId(notification.satelliteId);
navigate('/dashboard/satellites');
dismiss(notification.id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The action sets the selected satellite and then navigates to /dashboard/satellites, but that route renders the satellite table rather than EarthTwin. Consequently, the user is not taken to the globe or cinematic camera view promised by the TRACK LIVE action. [api mismatch]

Severity Level: Major ⚠️
- ❌ TRACK LIVE opens the satellite catalog instead of EarthTwin.
- ⚠️ Flyby users must manually navigate to the globe.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** frontend/src/components/ui/FlybyNotification.tsx
**Line:** 61:64
**Comment:**
	*Api Mismatch: The action sets the selected satellite and then navigates to `/dashboard/satellites`, but that route renders the satellite table rather than `EarthTwin`. Consequently, the user is not taken to the globe or cinematic camera view promised by the TRACK LIVE action.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

export function useFlybyEngine() {
const { selectedSatelliteIds } = useUIStore();
const { preferences, addNotification } = useNotificationStore();
const { bookmarks } = useBookmarkStorage();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: This hook creates its own independent useBookmarkStorage state. Bookmark updates performed through the globe's separate useBookmarks instance update local storage but do not update this captured bookmarks array, so newly added or edited targets are not considered by the flyby engine until the component is remounted. [stale reference]

Severity Level: Major ⚠️
- ❌ Newly added globe bookmarks are omitted from flyby detection.
- ⚠️ Edited bookmark coordinates remain stale for alerts.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** frontend/src/hooks/useFlybyEngine.ts
**Line:** 17:17
**Comment:**
	*Stale Reference: This hook creates its own independent `useBookmarkStorage` state. Bookmark updates performed through the globe's separate `useBookmarks` instance update local storage but do not update this captured `bookmarks` array, so newly added or edited targets are not considered by the flyby engine until the component is remounted.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +124 to +128
// Run immediately, then on interval
checkFlybys();
const intervalId = setInterval(checkFlybys, CHECK_INTERVAL_MS);

return () => clearInterval(intervalId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The asynchronous check continues after this effect is cleaned up because cleanup only clears the interval. If the selected satellites, bookmarks, or warning preference changes while Promise.allSettled or propagation is still running, the old closure can add alerts for deselected satellites or removed locations. [stale reference]

Severity Level: Major ⚠️
- ⚠️ Users can receive flyby alerts for deselected satellites.
- ⚠️ Removed bookmark locations can still generate alerts.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** frontend/src/hooks/useFlybyEngine.ts
**Line:** 124:128
**Comment:**
	*Stale Reference: The asynchronous check continues after this effect is cleaned up because cleanup only clears the interval. If the selected satellites, bookmarks, or warning preference changes while `Promise.allSettled` or propagation is still running, the old closure can add alerts for deselected satellites or removed locations.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

setActiveSector: (sector) => set({ activeSector: sector }),
setGlobalSearchOpen: (open) => set({ globalSearchOpen: open }),
toggleFlybyHistory: () => set((state) => ({ isFlybyHistoryOpen: !state.isFlybyHistoryOpen })),
setCameraMode: (mode) => set({ cameraMode: mode }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Camera mode is retained when selectedSatelliteId is cleared. After selecting CHASE, COCKPIT, or another cinematic mode, closing the selection hides CameraControls but leaves the mode active; selecting a different satellite later immediately re-enters the old mode unexpectedly. Reset cameraMode to FREE when selection is cleared, or explicitly reset it when a new satellite is selected. [state and lifecycle behavior]

Severity Level: Major ⚠️
- ⚠️ New satellite selections unexpectedly resume prior camera modes.
- ❌ Manual camera control is overridden after reselection.
- ⚠️ Camera controls disappear while stale mode remains active.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** frontend/src/store/uiStore.ts
**Line:** 70:70
**Comment:**
	*State And Lifecycle Behavior: Camera mode is retained when `selectedSatelliteId` is cleared. After selecting CHASE, COCKPIT, or another cinematic mode, closing the selection hides `CameraControls` but leaves the mode active; selecting a different satellite later immediately re-enters the old mode unexpectedly. Reset `cameraMode` to `FREE` when selection is cleared, or explicitly reset it when a new satellite is selected.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +17 to +26
const alt = obj.semimajor_axis - EARTH_RADIUS_KM;
if (alt < 0 || alt > 100000) return null;

const epochDate = obj.epoch ? new Date(obj.epoch) : new Date();
const now = new Date();
const elapsedDays = (now.getTime() - epochDate.getTime()) / 86400000 + (timeOffsetSec / 86400);
const currentMeanAnomaly = ((obj.mean_anomaly + (elapsedDays * obj.mean_motion * 360)) % 360) * Math.PI / 180;

const ecc = obj.eccentricity ?? 0;
const trueAnomaly = currentMeanAnomaly + 2 * ecc * Math.sin(currentMeanAnomaly);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The propagator always uses the semimajor axis as the satellite's geocentric radius and approximates true anomaly without applying the eccentric-orbit radius formula. For non-circular orbits, this produces incorrect altitude and ground-track positions, so the globe, camera tracking, and flyby predictions can all place the satellite incorrectly. Compute the instantaneous radius from eccentricity and true anomaly before returning alt. [logic error]

Severity Level: Major ⚠️
- ❌ Flyby predictions use incorrect closest-approach positions.
- ⚠️ Elevation and ETA notifications become inaccurate.
- ⚠️ Cinematic camera tracking follows incorrect altitude.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** frontend/src/utils/orbitCalc.ts
**Line:** 17:26
**Comment:**
	*Logic Error: The propagator always uses the semimajor axis as the satellite's geocentric radius and approximates true anomaly without applying the eccentric-orbit radius formula. For non-circular orbits, this produces incorrect altitude and ground-track positions, so the globe, camera tracking, and flyby predictions can all place the satellite incorrectly. Compute the instantaneous radius from eccentricity and true anomaly before returning `alt`.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (7)
frontend/src/utils/orbitCalc.ts (1)

70-91: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the dead elevation computation and the exploratory comments.

Lines 78-83 compute d, cosEl, and elRad, but the function returns the value from line 89. elRad is never read. The unused local will also fail a no-unused-vars lint rule. The comments on lines 85-87 record a thought process rather than the final behavior.

The returned atan2 formula is correct. Keep it and drop the rest.

♻️ Proposed cleanup
 export function calculateElevationAngle(satelliteAltKm: number, groundDistanceKm: number): number {
   const rE = EARTH_RADIUS_KM;
   const rS = EARTH_RADIUS_KM + satelliteAltKm;
-  
+
   // Central angle between observer and satellite's nadir
   const gammaRad = groundDistanceKm / rE;
-  
-  // Slant range (distance from observer to satellite)
-  const d = Math.sqrt(rE ** 2 + rS ** 2 - 2 * rE * rS * Math.cos(gammaRad));
-  
-  // Elevation angle calculation
-  const cosEl = (rS * Math.sin(gammaRad)) / d;
-  
-  let elRad = Math.acos(cosEl);
-  
-  // If gamma > 90 deg, the satellite is definitely below the horizon, but Math.acos handles 0 to PI.
-  // Actually, wait, a standard way is to use atan2 or just simple geometry:
-  // el = atan( (cos(gamma) - (rE / rS)) / sin(gamma) )
-  
+
+  // Elevation above the local horizon; negative when the satellite is below it.
   const el = Math.atan2(Math.cos(gammaRad) - (rE / rS), Math.sin(gammaRad));
   return el * (180 / Math.PI);
 }
🤖 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/utils/orbitCalc.ts` around lines 70 - 91, In
calculateElevationAngle, remove the unused d, cosEl, and elRad calculations,
along with the exploratory comments between them and the final return. Preserve
the existing gammaRad setup and returned atan2-based elevation formula
unchanged.
frontend/src/hooks/useCinematicCamera.ts (1)

51-52: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Reuse one position property instead of allocating per frame.

onPreRender runs on every rendered frame. Line 52 constructs a new Cesium.ConstantPositionProperty each time and reassigns entity.position. Each reassignment allocates an object and raises definitionChanged on the entity, which forces the visualizer to rebind the property. Create the property once per tracked entity and call setValue on it.

♻️ Proposed fix
+  const positionPropRef = useRef<Cesium.ConstantPositionProperty | null>(null);
+  const positionEntityIdRef = useRef<string | null>(null);
       // 2. Update physical entity position so it visibly moves!
-      entity.position = new Cesium.ConstantPositionProperty(p0);
+      if (positionEntityIdRef.current !== targetId || !positionPropRef.current) {
+        positionPropRef.current = new Cesium.ConstantPositionProperty(p0);
+        positionEntityIdRef.current = targetId;
+        entity.position = positionPropRef.current;
+      } else {
+        positionPropRef.current.setValue(p0);
+      }
🤖 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/hooks/useCinematicCamera.ts` around lines 51 - 52, Update the
tracked-entity setup and onPreRender logic in useCinematicCamera so each entity
creates and assigns one ConstantPositionProperty only once, then reuse that
property by calling setValue(p0) on every frame instead of reallocating and
reassigning entity.position.
frontend/src/store/notificationStore.ts (1)

55-55: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cap the notification history.

Line 55 prepends without limit. NotificationCenter renders every entry in the history panel on lines 111-124, so the list and the store both grow for the lifetime of the session. Trim to a fixed maximum.

♻️ Proposed fix
+const MAX_NOTIFICATIONS = 100;
-    return { notifications: [newNotification, ...state.notifications] };
+    return { notifications: [newNotification, ...state.notifications].slice(0, MAX_NOTIFICATIONS) };
🤖 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/store/notificationStore.ts` at line 55, Cap the notification
history in the state update that prepends new notifications, retaining only a
fixed maximum number of entries after adding newNotification. Reuse the store’s
existing maximum-limit constant or define one near the notification state, and
preserve newest-first ordering for NotificationCenter.
frontend/src/components/ui/NotificationCenter.tsx (2)

51-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Icon-only controls in this PR expose no accessible name. Every new interactive control renders a MaterialIcon as its only child, or hides its text label at small breakpoints. Assistive technology receives no name for these controls, and the toggles do not publish their state. Add type="button", an aria-label, and aria-pressed or aria-expanded where the control carries state.

  • frontend/src/components/ui/NotificationCenter.tsx#L51-L56: add aria-label="Close flyby alerts panel" to the panel close button.
  • frontend/src/components/ui/NotificationCenter.tsx#L66-L71: add aria-label="Toggle audio alerts" and aria-pressed={preferences.soundEnabled} to the audio toggle.
  • frontend/src/components/ui/FlybyNotification.tsx#L85-L90: add an aria-label that names the satellite being dismissed.
  • frontend/src/components/layouts/MainLayout.tsx#L258-L263: add aria-label="Toggle flyby alerts" and aria-expanded={isFlybyHistoryOpen} to the radar button.
  • frontend/src/components/ui/CameraControls.tsx#L29-L41: add aria-label={${mode.label} camera mode} and aria-pressed={isActive}, because line 40 hides the visible label below the sm breakpoint.
🤖 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/ui/NotificationCenter.tsx` around lines 51 - 56, Add
type="button" and accessible state/name attributes to all identified controls:
in frontend/src/components/ui/NotificationCenter.tsx lines 51-56 label the close
button “Close flyby alerts panel,” and lines 66-71 label the audio toggle
“Toggle audio alerts” with aria-pressed={preferences.soundEnabled}; in
frontend/src/components/ui/FlybyNotification.tsx lines 85-90 add an aria-label
naming the satellite being dismissed; in
frontend/src/components/layouts/MainLayout.tsx lines 258-263 label the radar
button “Toggle flyby alerts” with aria-expanded={isFlybyHistoryOpen}; and in
frontend/src/components/ui/CameraControls.tsx lines 29-41 label each mode button
with `${mode.label} camera mode` and set aria-pressed={isActive}.

27-33: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Make the exit animation apply to the AnimatePresence child.

AnimatePresence only animates its direct children, but the direct child here is a plain div on line 29. That removes dismissed notifications without the FlybyNotification exit animation. Move key, className, and the animation props to a motion child of AnimatePresence, or wrap FlybyNotification with a motion component that carries exit.

🤖 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/ui/NotificationCenter.tsx` around lines 27 - 33,
Update the AnimatePresence mapping in NotificationCenter so its direct child is
a motion component carrying the notification key, pointer-events class, and exit
animation props. Keep FlybyNotification rendered inside that motion wrapper so
dismissed notifications use its intended exit animation.
frontend/src/components/EarthTwin.tsx (1)

6-6: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared CatalogObject type in EarthTwin.tsx.

EarthTwin.tsx still declares a local CatalogObject that duplicates @/types/satellite, including the interface used by catalogMapRef and keplerToLatLonAlt(obj). Import CatalogObject from the shared type and remove the local declaration so future shape changes converge at one definition.

🤖 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/EarthTwin.tsx` at line 6, Update EarthTwin.tsx to
import and use the shared CatalogObject type from `@/types/satellite` for
catalogMapRef and keplerToLatLonAlt(obj), then remove the duplicate local
CatalogObject declaration.
frontend/src/components/ui/CameraControls.tsx (1)

3-3: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Import CameraMode as a type.

CameraMode is exported only as a type, but CameraControls.tsx imports it in the value position while verbatimModuleSyntax is enabled. This leaves an unresolved runtime import for a removed value import.

♻️ Proposed fix
-import { useUIStore, CameraMode } from '`@/store/uiStore`';
+import { useUIStore } from '`@/store/uiStore`';
+import type { CameraMode } from '`@/store/uiStore`';
🤖 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/ui/CameraControls.tsx` at line 3, Update the import
in CameraControls to import CameraMode as a type while retaining the value
import for useUIStore, ensuring no runtime import is generated for the type-only
symbol.
🤖 Prompt for all review comments with 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.

Inline comments:
In @.gitignore:
- Line 100: Update the .gitignore entry for alembic.ini so the repository
includes a tracked backend Alembic configuration or shared template required by
the README migration setup; if ignoring it is intentional, ensure setup
generates a fixed repository-shared config while keeping secrets in environment
variables.

In `@frontend/src/components/layouts/MainLayout.tsx`:
- Around line 416-417: Update MainLayout so CameraControls is rendered only on
routes that also render EarthTwin, rather than unconditionally in the
application shell. Move it alongside the globe rendering or gate it using the
existing route condition, while preserving NotificationCenter and the current
camera-control behavior on globe routes.

In `@frontend/src/components/ui/FlybyNotification.tsx`:
- Around line 13-37: Update playBeep to close the newly created AudioContext
when the oscillator finishes, using the oscillator’s completion event after its
scheduled stop. Preserve the existing audio setup and warning behavior while
ensuring each notification releases its context resources.
- Around line 45-59: Update the FlybyNotification effect to guard all
Notification API access with a window availability check, remove
Notification.requestPermission from the effect, and ensure the alert fires only
once per notification rather than when sound preferences change. Use a ref or
equivalent keyed to the notification identity, remove the unused preferences
binding, and adjust the effect dependencies so audio toggles do not re-trigger
the beep or browser notification; permission requests should remain in an
explicit preferences-panel control.

In `@frontend/src/hooks/useCinematicCamera.ts`:
- Around line 124-138: Update the orientation interpolation in the cinematic
camera update around the Cesium.Cartesian3.lerp calls: after interpolating and
normalizing nextDir, rebuild nextUp using the cross product with nextDir and the
interpolated up reference, then normalize it so direction and up form an
orthonormal basis before viewer.camera.setView.

In `@frontend/src/hooks/useFlybyEngine.ts`:
- Around line 44-53: Refactor checkFlybys so catalog elements are fetched and
cached on a substantially longer refresh interval, while the existing 30-second
tick only propagates cached elements. Add AbortController cancellation to the
effect cleanup, pass its signal through in-flight work, and verify it is not
aborted before addNotification; update the interval lifecycle around the effect
containing checkFlybys. Refine the propagation grid in checkFlybys with a coarse
scan followed by a finer scan around the best sample so maxElevation captures
closer approaches.
- Around line 31-38: Update the geolocation failure and unavailable branches in
useFlybyEngine so userLocationRef.current remains null instead of being set to {
lat: 0, lon: 0 }; preserve the existing targetLocations behavior that skips the
current-location entry when the ref is null, allowing only bookmarked locations
to drive predictions.
- Line 51: Update the fulfilled-result handling in useFlybyEngine to remove the
runtime any cast before calling keplerToLatLonAlt. Pass the API object through
its existing SpaceObject-compatible shape, or introduce a shared typed interface
for the conversion, while preserving the current propagation behavior and
avoiding any.

In `@frontend/src/store/notificationStore.ts`:
- Around line 37-46: Update the duplicate detection in addNotification to match
notifications by satelliteId, locationName, and nearby eta regardless of the
dismissed flag. Remove the !n.dismissed condition so dismissNotification records
continue suppressing repeated engine-tick notifications.

---

Nitpick comments:
In `@frontend/src/components/EarthTwin.tsx`:
- Line 6: Update EarthTwin.tsx to import and use the shared CatalogObject type
from `@/types/satellite` for catalogMapRef and keplerToLatLonAlt(obj), then remove
the duplicate local CatalogObject declaration.

In `@frontend/src/components/ui/CameraControls.tsx`:
- Line 3: Update the import in CameraControls to import CameraMode as a type
while retaining the value import for useUIStore, ensuring no runtime import is
generated for the type-only symbol.

In `@frontend/src/components/ui/NotificationCenter.tsx`:
- Around line 51-56: Add type="button" and accessible state/name attributes to
all identified controls: in frontend/src/components/ui/NotificationCenter.tsx
lines 51-56 label the close button “Close flyby alerts panel,” and lines 66-71
label the audio toggle “Toggle audio alerts” with
aria-pressed={preferences.soundEnabled}; in
frontend/src/components/ui/FlybyNotification.tsx lines 85-90 add an aria-label
naming the satellite being dismissed; in
frontend/src/components/layouts/MainLayout.tsx lines 258-263 label the radar
button “Toggle flyby alerts” with aria-expanded={isFlybyHistoryOpen}; and in
frontend/src/components/ui/CameraControls.tsx lines 29-41 label each mode button
with `${mode.label} camera mode` and set aria-pressed={isActive}.
- Around line 27-33: Update the AnimatePresence mapping in NotificationCenter so
its direct child is a motion component carrying the notification key,
pointer-events class, and exit animation props. Keep FlybyNotification rendered
inside that motion wrapper so dismissed notifications use its intended exit
animation.

In `@frontend/src/hooks/useCinematicCamera.ts`:
- Around line 51-52: Update the tracked-entity setup and onPreRender logic in
useCinematicCamera so each entity creates and assigns one
ConstantPositionProperty only once, then reuse that property by calling
setValue(p0) on every frame instead of reallocating and reassigning
entity.position.

In `@frontend/src/store/notificationStore.ts`:
- Line 55: Cap the notification history in the state update that prepends new
notifications, retaining only a fixed maximum number of entries after adding
newNotification. Reuse the store’s existing maximum-limit constant or define one
near the notification state, and preserve newest-first ordering for
NotificationCenter.

In `@frontend/src/utils/orbitCalc.ts`:
- Around line 70-91: In calculateElevationAngle, remove the unused d, cosEl, and
elRad calculations, along with the exploratory comments between them and the
final return. Preserve the existing gammaRad setup and returned atan2-based
elevation formula unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e3e6ae86-1b19-4018-8a05-813fc611de51

📥 Commits

Reviewing files that changed from the base of the PR and between 92321d4 and 5ee1046.

⛔ Files ignored due to path filters (27)
  • backend/api/v1/__pycache__/__init__.cpython-313.pyc is excluded by !**/*.pyc
  • backend/api/v1/endpoints/__pycache__/__init__.cpython-313.pyc is excluded by !**/*.pyc
  • backend/api/v1/endpoints/__pycache__/agents.cpython-313.pyc is excluded by !**/*.pyc
  • backend/api/v1/endpoints/__pycache__/auth.cpython-313.pyc is excluded by !**/*.pyc
  • backend/api/v1/endpoints/__pycache__/catalog.cpython-313.pyc is excluded by !**/*.pyc
  • backend/api/v1/endpoints/__pycache__/collisions.cpython-313.pyc is excluded by !**/*.pyc
  • backend/api/v1/endpoints/__pycache__/dashboard.cpython-313.pyc is excluded by !**/*.pyc
  • backend/api/v1/endpoints/__pycache__/satellites.cpython-313.pyc is excluded by !**/*.pyc
  • backend/api/v1/endpoints/__pycache__/weather.cpython-313.pyc is excluded by !**/*.pyc
  • backend/app/__pycache__/__init__.cpython-313.pyc is excluded by !**/*.pyc
  • backend/app/__pycache__/main.cpython-313.pyc is excluded by !**/*.pyc
  • backend/app/core/__pycache__/__init__.cpython-313.pyc is excluded by !**/*.pyc
  • backend/app/core/__pycache__/config.cpython-313.pyc is excluded by !**/*.pyc
  • backend/app/core/__pycache__/scheduler.cpython-313.pyc is excluded by !**/*.pyc
  • backend/app/core/__pycache__/security.cpython-313.pyc is excluded by !**/*.pyc
  • backend/database/__pycache__/__init__.cpython-313.pyc is excluded by !**/*.pyc
  • backend/database/__pycache__/session.cpython-313.pyc is excluded by !**/*.pyc
  • backend/models/__pycache__/__init__.cpython-313.pyc is excluded by !**/*.pyc
  • backend/models/__pycache__/db_models.cpython-313.pyc is excluded by !**/*.pyc
  • backend/orbital/__pycache__/__init__.cpython-313.pyc is excluded by !**/*.pyc
  • backend/orbital/providers/__pycache__/__init__.cpython-313.pyc is excluded by !**/*.pyc
  • backend/orbital/providers/__pycache__/base.cpython-313.pyc is excluded by !**/*.pyc
  • backend/orbital/providers/__pycache__/cache.cpython-313.pyc is excluded by !**/*.pyc
  • backend/orbital/providers/__pycache__/celestrak.cpython-313.pyc is excluded by !**/*.pyc
  • backend/orbital/providers/__pycache__/chain.cpython-313.pyc is excluded by !**/*.pyc
  • backend/orbital/providers/__pycache__/spacetrack.cpython-313.pyc is excluded by !**/*.pyc
  • backend/tests/__pycache__/test_backend.cpython-312-pytest-9.1.1.pyc is excluded by !**/*.pyc
📒 Files selected for processing (12)
  • .gitignore
  • frontend/src/components/EarthTwin.tsx
  • frontend/src/components/layouts/MainLayout.tsx
  • frontend/src/components/ui/CameraControls.tsx
  • frontend/src/components/ui/FlybyNotification.tsx
  • frontend/src/components/ui/NotificationCenter.tsx
  • frontend/src/hooks/useCinematicCamera.ts
  • frontend/src/hooks/useFlybyEngine.ts
  • frontend/src/store/notificationStore.ts
  • frontend/src/store/uiStore.ts
  • frontend/src/utils/orbitCalc.ts
  • frontend/tsconfig.app.json
💤 Files with no reviewable changes (1)
  • frontend/tsconfig.app.json

Comment thread .gitignore
env/
.env
.pytest_cache/
alembic.ini

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files | rg '(^|/)alembic\.ini$|(^|/)alembic(/|$)' || true
rg -n --hidden --glob '!.git/**' 'alembic\.ini|alembic' . || true
git check-ignore -v --no-index alembic.ini || true

Repository: 7-Blocks/Kepler

Length of output: 259


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Tracked Alembic-related files:\n'
git ls-files | rg '(^|/)alembic\.ini$|(^|/)alembic(/|$)|alembic|alembic' || true

printf '\nREADME Alembic context:\n'
sed -n '260,300p' README.md 2>/dev/null || true

printf '\nALEMBIC_CONFIG references:\n'
rg -n --hidden --glob '!.git/**' 'ALEMBIC_CONFIG|alembic\.ini|alembic upgrade|alembic init|alembic_version|alembic' . \
  --glob '!/.git/**' || true

printf '\nRoot/config files:\n'
git ls-files '*/' | sed 's#/.*##' | sort -u | rg '^(alembic|alembic\.ini|pyproject|setup\.py|requirements|Pipfile|poetry\.lock)$' || true

Repository: 7-Blocks/Kepler

Length of output: 948


Keep alembic.ini tracked or provide a repository-shared template.

README step 9 runs alembic upgrade head in backend, but the repository does not include backend/alembic.ini or another shared Alembic config. Keep ignoring alembic.ini only if the repository intentionally provides a fixed generated config during setup. Otherwise, commit the migration config/template and keep secrets in environment variables.

🤖 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 @.gitignore at line 100, Update the .gitignore entry for alembic.ini so the
repository includes a tracked backend Alembic configuration or shared template
required by the README migration setup; if ignoring it is intentional, ensure
setup generates a fixed repository-shared config while keeping secrets in
environment variables.

Comment on lines +416 to +417
<NotificationCenter />
<CameraControls />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Render CameraControls only where a globe exists.

Line 417 mounts CameraControls in the application shell, so it renders on every route under this layout. The component shows itself whenever selectedSatelliteId is set (frontend/src/components/ui/CameraControls.tsx line 19). The camera modes act on the Cesium viewer that EarthTwin owns, through useCinematicCamera. On a route without the globe, such as /dashboard/satellites or /dashboard/settings, a previously selected satellite still makes the control bar appear, and every mode button then produces no visible result.

Move CameraControls next to the globe, or gate it on the route that renders EarthTwin.

🤖 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/layouts/MainLayout.tsx` around lines 416 - 417,
Update MainLayout so CameraControls is rendered only on routes that also render
EarthTwin, rather than unconditionally in the application shell. Move it
alongside the globe rendering or gate it using the existing route condition,
while preserving NotificationCenter and the current camera-control behavior on
globe routes.

Comment on lines +13 to +37
const playBeep = () => {
try {
const AudioContext = window.AudioContext || (window as any).webkitAudioContext;
if (!AudioContext) return;

const ctx = new AudioContext();
const osc = ctx.createOscillator();
const gainNode = ctx.createGain();

osc.type = 'sine';
osc.frequency.setValueAtTime(880, ctx.currentTime); // A5
osc.frequency.exponentialRampToValueAtTime(440, ctx.currentTime + 0.1); // Drop to A4

gainNode.gain.setValueAtTime(0.1, ctx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.5);

osc.connect(gainNode);
gainNode.connect(ctx.destination);

osc.start();
osc.stop(ctx.currentTime + 0.5);
} catch (e) {
console.warn('Audio play failed', e);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Close the AudioContext after the beep.

Line 18 creates a new AudioContext on every notification and never closes it. Browsers limit the number of concurrent AudioContext instances per document. Chrome throws once the limit is reached. The catch on line 34 swallows that error, so alert audio stops working silently after a few notifications.

Close the context when the oscillator ends, or create one context at module scope and reuse it.

🐛 Proposed fix
     osc.start();
     osc.stop(ctx.currentTime + 0.5);
+    osc.onended = () => {
+      void ctx.close();
+    };
   } catch (e) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const playBeep = () => {
try {
const AudioContext = window.AudioContext || (window as any).webkitAudioContext;
if (!AudioContext) return;
const ctx = new AudioContext();
const osc = ctx.createOscillator();
const gainNode = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(880, ctx.currentTime); // A5
osc.frequency.exponentialRampToValueAtTime(440, ctx.currentTime + 0.1); // Drop to A4
gainNode.gain.setValueAtTime(0.1, ctx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.5);
osc.connect(gainNode);
gainNode.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 0.5);
} catch (e) {
console.warn('Audio play failed', e);
}
};
const playBeep = () => {
try {
const AudioContext = window.AudioContext || (window as any).webkitAudioContext;
if (!AudioContext) return;
const ctx = new AudioContext();
const osc = ctx.createOscillator();
const gainNode = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(880, ctx.currentTime); // A5
osc.frequency.exponentialRampToValueAtTime(440, ctx.currentTime + 0.1); // Drop to A4
gainNode.gain.setValueAtTime(0.1, ctx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.5);
osc.connect(gainNode);
gainNode.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 0.5);
osc.onended = () => {
void ctx.close();
};
} catch (e) {
console.warn('Audio play failed', e);
}
};
🤖 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/ui/FlybyNotification.tsx` around lines 13 - 37,
Update playBeep to close the newly created AudioContext when the oscillator
finishes, using the oscillator’s completion event after its scheduled stop.
Preserve the existing audio setup and warning behavior while ensuring each
notification releases its context resources.

Comment on lines +45 to +59
useEffect(() => {
if (preferences.soundEnabled) {
playBeep();
}

// Optional: Use browser notifications API if permitted
if (Notification.permission === 'granted') {
new Notification(`Flyby Alert: ${notification.satelliteName}`, {
body: `Approaching ${notification.locationName}. ETA: ${notification.eta.toLocaleTimeString()}`,
icon: '/vite.svg'
});
} else if (Notification.permission !== 'denied') {
Notification.requestPermission();
}
}, [notification, preferences.soundEnabled]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard the Notification global, and do not re-fire the alert when the user toggles audio.

Two problems in this effect:

  1. Lines 51-57 read Notification.permission without checking that the API exists. The Notification API is absent in non-secure contexts and in some mobile browsers. The unguarded access throws inside the effect and breaks the component render. Check 'Notification' in window first.

  2. The dependency array on line 59 includes preferences.soundEnabled. When the user toggles audio in NotificationCenter, this effect re-runs for every mounted notification. The beep plays again and a second browser notification appears for the same pass. Fire the alert once per notification.

Line 57 also calls Notification.requestPermission() during render of a notification rather than from a user gesture. Several browsers reject a permission request that has no user activation. Move the request to an explicit control in the preferences panel.

🐛 Proposed fix
+  const alertedRef = useRef(false);
+
   useEffect(() => {
-    if (preferences.soundEnabled) {
+    // Alert once per notification. Reading soundEnabled from the store here
+    // keeps the toggle out of the dependency array, so flipping it does not
+    // replay the alert for every mounted notification.
+    if (alertedRef.current) return;
+    alertedRef.current = true;
+
+    if (useNotificationStore.getState().preferences.soundEnabled) {
       playBeep();
     }
-    
-    // Optional: Use browser notifications API if permitted
-    if (Notification.permission === 'granted') {
+
+    // Optional: Use browser notifications API if available and permitted.
+    if (!('Notification' in window)) return;
+
+    if (Notification.permission === 'granted') {
       new Notification(`Flyby Alert: ${notification.satelliteName}`, {
         body: `Approaching ${notification.locationName}. ETA: ${notification.eta.toLocaleTimeString()}`,
         icon: '/vite.svg'
       });
-    } else if (Notification.permission !== 'denied') {
-      Notification.requestPermission();
     }
-  }, [notification, preferences.soundEnabled]);
+  }, [notification]);

Update the import on line 1 to include useRef, and drop the now-unused preferences binding on line 41 if nothing else reads it.

🤖 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/ui/FlybyNotification.tsx` around lines 45 - 59,
Update the FlybyNotification effect to guard all Notification API access with a
window availability check, remove Notification.requestPermission from the
effect, and ensure the alert fires only once per notification rather than when
sound preferences change. Use a ref or equivalent keyed to the notification
identity, remove the unused preferences binding, and adjust the effect
dependencies so audio toggles do not re-trigger the beep or browser
notification; permission requests should remain in an explicit preferences-panel
control.

Comment on lines +124 to +138
Cesium.Cartesian3.lerp(currentPos, targetPos, LERP_FACTOR, nextPos);
Cesium.Cartesian3.lerp(currentDir, targetDir, LERP_FACTOR, nextDir);
Cesium.Cartesian3.lerp(currentUp, targetUp, LERP_FACTOR, nextUp);

Cesium.Cartesian3.normalize(nextDir, nextDir);
Cesium.Cartesian3.normalize(nextUp, nextUp);

// Force the camera view
viewer.camera.setView({
destination: nextPos,
orientation: {
direction: nextDir,
up: nextUp,
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Cesium Camera setView orientation direction up must be orthogonal

💡 Result:

In CesiumJS, when using the setView or flyTo methods with a custom orientation defined by direction and up vectors, these vectors must form an orthonormal basis [1][2][3]. Specifically, the direction and up vectors must be orthogonal (at a 90-degree angle to each other) and both must be normalized (unit length) [1][3]. The camera's internal orientation is maintained as an orthonormal basis, where: - The view direction is represented by the direction vector [1][2]. - The up vector defines the camera's vertical orientation [2][3]. - A third vector, right, is automatically calculated as the cross product of direction and up (right = direction x up) [1][2]. If the provided direction and up vectors are not orthogonal, the resulting camera orientation may be inconsistent or behave unexpectedly, as the engine requires a valid orthonormal basis to correctly construct the view matrix [1][3]. When manually providing these vectors, ensure they are unit vectors and satisfy the dot product condition (dot(direction, up) == 0).

Citations:


Don’t interpolate direction and up independently.

Cesium’s Camera.setView orientation contract requires normalized direction and up to form an orthonormal basis. Lines 125-126 move each vector toward its target separately, and lines 128-129 only normalize them independently, so setView receives a pair whose relative angle drifts out of orthogonal. Rebuild up from the cross product after interpolating before passing it to setView.

♻️ Proposed fix
       Cesium.Cartesian3.normalize(nextDir, nextDir);
-      Cesium.Cartesian3.normalize(nextUp, nextUp);
+      // Rebuild an orthonormal basis: independent lerp of dir/up loses perpendicularity.
+      const right = Cesium.Cartesian3.cross(nextDir, nextUp, new Cesium.Cartesian3());
+      Cesium.Cartesian3.normalize(right, right);
+      Cesium.Cartesian3.cross(right, nextDir, nextUp);
+      Cesium.Cartesian3.normalize(nextUp, nextUp);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Cesium.Cartesian3.lerp(currentPos, targetPos, LERP_FACTOR, nextPos);
Cesium.Cartesian3.lerp(currentDir, targetDir, LERP_FACTOR, nextDir);
Cesium.Cartesian3.lerp(currentUp, targetUp, LERP_FACTOR, nextUp);
Cesium.Cartesian3.normalize(nextDir, nextDir);
Cesium.Cartesian3.normalize(nextUp, nextUp);
// Force the camera view
viewer.camera.setView({
destination: nextPos,
orientation: {
direction: nextDir,
up: nextUp,
}
});
Cesium.Cartesian3.lerp(currentPos, targetPos, LERP_FACTOR, nextPos);
Cesium.Cartesian3.lerp(currentDir, targetDir, LERP_FACTOR, nextDir);
Cesium.Cartesian3.lerp(currentUp, targetUp, LERP_FACTOR, nextUp);
Cesium.Cartesian3.normalize(nextDir, nextDir);
// Rebuild an orthonormal basis: independent lerp of dir/up loses perpendicularity.
const right = Cesium.Cartesian3.cross(nextDir, nextUp, new Cesium.Cartesian3());
Cesium.Cartesian3.normalize(right, right);
Cesium.Cartesian3.cross(right, nextDir, nextUp);
Cesium.Cartesian3.normalize(nextUp, nextUp);
// Force the camera view
viewer.camera.setView({
destination: nextPos,
orientation: {
direction: nextDir,
up: nextUp,
}
});
🤖 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/hooks/useCinematicCamera.ts` around lines 124 - 138, Update the
orientation interpolation in the cinematic camera update around the
Cesium.Cartesian3.lerp calls: after interpolating and normalizing nextDir,
rebuild nextUp using the cross product with nextDir and the interpolated up
reference, then normalize it so direction and up form an orthonormal basis
before viewer.camera.setView.

Comment on lines +31 to +38
(error) => {
console.warn('Geolocation denied or failed, using default location (0,0).', error);
userLocationRef.current = { lat: 0, lon: 0 };
}
);
} else {
userLocationRef.current = { lat: 0, lon: 0 };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not fall back to latitude 0, longitude 0 when geolocation is unavailable.

If the user denies geolocation, lines 33 and 37 set the observer to (0, 0). That point is in the Gulf of Guinea. The engine then predicts passes over it and labels them "Current Location" on line 58, so the user receives alerts for a place they never selected. The targetLocations builder on lines 57-61 already skips the entry when userLocationRef.current is null.

Leave the ref null and let bookmarks drive the predictions.

🐛 Proposed fix
         (error) => {
-          console.warn('Geolocation denied or failed, using default location (0,0).', error);
-          userLocationRef.current = { lat: 0, lon: 0 };
+          // Leave the ref null: flyby predictions then use bookmarked
+          // locations only, instead of an arbitrary point at (0, 0).
+          console.warn('Geolocation denied or failed; flyby alerts will use bookmarks only.', error);
+          userLocationRef.current = null;
         }
       );
     } else {
-      userLocationRef.current = { lat: 0, lon: 0 };
+      userLocationRef.current = null;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(error) => {
console.warn('Geolocation denied or failed, using default location (0,0).', error);
userLocationRef.current = { lat: 0, lon: 0 };
}
);
} else {
userLocationRef.current = { lat: 0, lon: 0 };
}
(error) => {
// Leave the ref null: flyby predictions then use bookmarked
// locations only, instead of an arbitrary point at (0, 0).
console.warn('Geolocation denied or failed; flyby alerts will use bookmarks only.', error);
userLocationRef.current = null;
}
);
} else {
userLocationRef.current = null;
}
🤖 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/hooks/useFlybyEngine.ts` around lines 31 - 38, Update the
geolocation failure and unavailable branches in useFlybyEngine so
userLocationRef.current remains null instead of being set to { lat: 0, lon: 0 };
preserve the existing targetLocations behavior that skips the current-location
entry when the ref is null, allowing only bookmarked locations to drive
predictions.

Comment on lines +44 to +53
const checkFlybys = async () => {
try {
// Fetch data for all tracked satellites
const satPromises = selectedSatelliteIds.map(id => api.getCatalogObjectByNorad(id));
const responses = await Promise.allSettled(satPromises);

const satellites = responses
.filter((res): res is PromiseFulfilledResult<any> => res.status === 'fulfilled')
.map(res => res.value.data as SpaceObject)
.filter(Boolean);

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 | 🏗️ Heavy lift

Decouple the catalog fetch from the 30-second prediction tick, and cancel in-flight work.

checkFlybys runs every 30 seconds (line 126) and issues one API request per selected satellite (line 47). Orbital elements change on the order of hours, so the refetch produces no accuracy gain and multiplies backend requests by the number of tracked satellites.

The effect cleanup on line 128 clears the interval only. It does not cancel a request that is already in flight. After the user changes the selection or the component unmounts, the pending checkFlybys still resolves and still calls addNotification on line 105. Notifications for a stale selection then appear.

Cache the fetched elements and refresh them on a long interval. Propagate from the cached elements on the 30-second tick. Add an AbortController and check it before calling addNotification.

Separately, the propagation grid on line 77 steps one minute at a time. A LEO satellite traverses the visible sky in a few minutes, so the sampled minimum can miss the true closest approach and understate maxElevation at the gate on line 98. Consider a coarse pass followed by a finer scan around the best sample.

Also applies to: 124-129

🤖 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/hooks/useFlybyEngine.ts` around lines 44 - 53, Refactor
checkFlybys so catalog elements are fetched and cached on a substantially longer
refresh interval, while the existing 30-second tick only propagates cached
elements. Add AbortController cancellation to the effect cleanup, pass its
signal through in-flight work, and verify it is not aborted before
addNotification; update the interval lifecycle around the effect containing
checkFlybys. Refine the propagation grid in checkFlybys with a coarse scan
followed by a finer scan around the best sample so maxElevation captures closer
approaches.

const responses = await Promise.allSettled(satPromises);

const satellites = responses
.filter((res): res is PromiseFulfilledResult<any> => res.status === 'fulfilled')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Compare SpaceObject with the CatalogObject fields that keplerToLatLonAlt reads.
set -euo pipefail

echo '--- SpaceObject ---'
ast-grep run --pattern 'interface SpaceObject { $$$ }' --lang typescript frontend/src || true
ast-grep run --pattern 'type SpaceObject = $$$' --lang typescript frontend/src || true

echo '--- CatalogObject in shared types ---'
fd -t f 'satellite.ts' frontend/src/types --exec cat -n {}

echo '--- getCatalogObjectByNorad signature ---'
rg -nP --type=ts -C5 'getCatalogObjectByNorad' frontend/src

Repository: 7-Blocks/Kepler

Length of output: 6187


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- useFlybyEngine outline ---'
ast-grep outline frontend/src/hooks/useFlybyEngine.ts || true

echo '--- useFlybyEngine relevant source ---'
sed -n '1,120p' frontend/src/hooks/useFlybyEngine.ts | cat -n

echo '--- orbital propagator definitions/usages ---'
rg -n --type=ts -C3 'keplerToLatLonAlt|CatalogObject|SpaceObject|semimajor_axis|arg_of_perigee|mean_motion|inclination|mean_anomaly|raan|eccentricity|epoch' frontend/src

Repository: 7-Blocks/Kepler

Length of output: 50371


Remove the runtime any cast before Kepler propagation.

SpaceObject already contains the orbit fields keplerToLatLonAlt reads, so sat as any is unnecessary and hides future field changes. Let keplerToLatLonAlt receive the API object with an any-free conversion if a shared interface is desired.

🤖 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/hooks/useFlybyEngine.ts` at line 51, Update the fulfilled-result
handling in useFlybyEngine to remove the runtime any cast before calling
keplerToLatLonAlt. Pass the API object through its existing
SpaceObject-compatible shape, or introduce a shared typed interface for the
conversion, while preserving the current propagation behavior and avoiding any.

Comment on lines +37 to +46
addNotification: (notificationData) => set((state) => {
// Avoid duplicate active notifications for the same satellite and location within a short timeframe
const isDuplicate = state.notifications.some(
(n) => n.satelliteId === notificationData.satelliteId &&
n.locationName === notificationData.locationName &&
!n.dismissed &&
Math.abs(n.eta.getTime() - notificationData.eta.getTime()) < 5 * 60 * 1000 // 5 min window
);

if (isDuplicate) return state;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

A dismissed notification reappears on the next engine tick.

The duplicate check on lines 39-44 requires !n.dismissed. dismissNotification on lines 57-61 sets dismissed: true and keeps the record in the array. The flyby engine re-evaluates the same pass every 30 seconds and calls addNotification again with a near-identical eta. The dismissed record no longer matches the predicate, so the store creates a new notification and NotificationCenter shows the toast again.

The user dismisses an alert and it returns within 30 seconds, for as long as the pass stays inside the warning window.

Match against dismissed notifications too when suppressing duplicates.

🐛 Proposed fix
   addNotification: (notificationData) => set((state) => {
-    // Avoid duplicate active notifications for the same satellite and location within a short timeframe
+    // Suppress duplicates for the same satellite and location within a short
+    // timeframe. Dismissed notifications are included: a dismissed pass must
+    // not reappear when the engine re-evaluates it on the next tick.
     const isDuplicate = state.notifications.some(
       (n) => n.satelliteId === notificationData.satelliteId && 
              n.locationName === notificationData.locationName &&
-             !n.dismissed &&
              Math.abs(n.eta.getTime() - notificationData.eta.getTime()) < 5 * 60 * 1000 // 5 min window
     );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
addNotification: (notificationData) => set((state) => {
// Avoid duplicate active notifications for the same satellite and location within a short timeframe
const isDuplicate = state.notifications.some(
(n) => n.satelliteId === notificationData.satelliteId &&
n.locationName === notificationData.locationName &&
!n.dismissed &&
Math.abs(n.eta.getTime() - notificationData.eta.getTime()) < 5 * 60 * 1000 // 5 min window
);
if (isDuplicate) return state;
addNotification: (notificationData) => set((state) => {
// Suppress duplicates for the same satellite and location within a short
// timeframe. Dismissed notifications are included: a dismissed pass must
// not reappear when the engine re-evaluates it on the next tick.
const isDuplicate = state.notifications.some(
(n) => n.satelliteId === notificationData.satelliteId &&
n.locationName === notificationData.locationName &&
Math.abs(n.eta.getTime() - notificationData.eta.getTime()) < 5 * 60 * 1000 // 5 min window
);
if (isDuplicate) return state;
🤖 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/store/notificationStore.ts` around lines 37 - 46, Update the
duplicate detection in addNotification to match notifications by satelliteId,
locationName, and nearby eta regardless of the dismissed flag. Remove the
!n.dismissed condition so dismissNotification records continue suppressing
repeated engine-tick notifications.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a flyby-alert notification system and a cinematic satellite-tracking camera mode to the Kepler globe UI, introducing new Zustand state, background polling, and new UI panels/controls.

Changes:

  • Added a flyby engine that periodically predicts near-term passes over the user’s current location and bookmarks and raises alerts.
  • Introduced a cinematic camera hook + UI controls for multiple camera perspectives (Free/Chase/Cockpit/Earth Observer/Orbital).
  • Refactored orbit propagation helpers into a shared utility module and integrated the new UI surfaces into the main layout.

Reviewed changes

Copilot reviewed 11 out of 39 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
frontend/tsconfig.app.json Removes ignoreDeprecations from app TS config.
frontend/src/utils/orbitCalc.ts Adds shared orbital math helpers (propagation, distance, elevation).
frontend/src/store/uiStore.ts Adds UI state for flyby history panel + camera mode selection.
frontend/src/store/notificationStore.ts Adds Zustand store for flyby notifications and user preferences.
frontend/src/hooks/useFlybyEngine.ts Implements periodic flyby prediction + notification creation.
frontend/src/hooks/useCinematicCamera.ts Implements per-frame satellite tracking + camera interpolation modes.
frontend/src/components/ui/NotificationCenter.tsx Adds flyby toasts + history/settings panel UI.
frontend/src/components/ui/FlybyNotification.tsx Adds individual toast rendering + sound/browser notification behavior.
frontend/src/components/ui/CameraControls.tsx Adds bottom-center camera mode switcher UI.
frontend/src/components/layouts/MainLayout.tsx Mounts notification center + camera controls; adds toolbar entry to toggle panel.
frontend/src/components/EarthTwin.tsx Integrates spotlight + cinematic camera hooks; switches to shared orbit util.
.gitignore Adds common Python-related ignores.
Suppressed comments (3)

frontend/src/components/ui/FlybyNotification.tsx:58

  • Requesting notification permission as a side effect of showing an alert can repeatedly prompt the user (one prompt per flyby toast) and is blocked by some browsers unless triggered by explicit user interaction. Prefer only showing notifications when permission is already granted; if you want to request permission, do it from an explicit UI action in the settings panel.
    frontend/src/components/ui/NotificationCenter.tsx:70
  • The audio toggle is an icon-only button without an accessible name. Add an aria-label describing the current state/action.
    frontend/src/components/ui/FlybyNotification.tsx:89
  • The dismiss (X) button is icon-only and lacks an accessible name. Add an aria-label so it’s usable with screen readers.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +70 to +91
export function calculateElevationAngle(satelliteAltKm: number, groundDistanceKm: number): number {
const rE = EARTH_RADIUS_KM;
const rS = EARTH_RADIUS_KM + satelliteAltKm;

// Central angle between observer and satellite's nadir
const gammaRad = groundDistanceKm / rE;

// Slant range (distance from observer to satellite)
const d = Math.sqrt(rE ** 2 + rS ** 2 - 2 * rE * rS * Math.cos(gammaRad));

// Elevation angle calculation
const cosEl = (rS * Math.sin(gammaRad)) / d;

let elRad = Math.acos(cosEl);

// If gamma > 90 deg, the satellite is definitely below the horizon, but Math.acos handles 0 to PI.
// Actually, wait, a standard way is to use atan2 or just simple geometry:
// el = atan( (cos(gamma) - (rE / rS)) / sin(gamma) )

const el = Math.atan2(Math.cos(gammaRad) - (rE / rS), Math.sin(gammaRad));
return el * (180 / Math.PI);
}
import { useEffect, useRef } from 'react';
import * as Cesium from 'cesium';
import { useUIStore, CameraMode } from '@/store/uiStore';
import { CatalogObject } from '@/types/satellite';
Comment on lines +124 to +127
// Run immediately, then on interval
checkFlybys();
const intervalId = setInterval(checkFlybys, CHECK_INTERVAL_MS);

Comment on lines +13 to +37
const playBeep = () => {
try {
const AudioContext = window.AudioContext || (window as any).webkitAudioContext;
if (!AudioContext) return;

const ctx = new AudioContext();
const osc = ctx.createOscillator();
const gainNode = ctx.createGain();

osc.type = 'sine';
osc.frequency.setValueAtTime(880, ctx.currentTime); // A5
osc.frequency.exponentialRampToValueAtTime(440, ctx.currentTime + 0.1); // Drop to A4

gainNode.gain.setValueAtTime(0.1, ctx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.5);

osc.connect(gainNode);
gainNode.connect(ctx.destination);

osc.start();
osc.stop(ctx.currentTime + 0.5);
} catch (e) {
console.warn('Audio play failed', e);
}
};
Comment on lines 2 to 7
import { prefersReducedMotion } from './SatelliteSpotlight/GlowEffect';
import { useSatelliteSelection } from '@/hooks/useSatelliteSelection';
import { useSpotlightEffect } from '@/hooks/useSpotlightEffect';
import { useCinematicCamera } from '@/hooks/useCinematicCamera';
import { keplerToLatLonAlt } from '@/utils/orbitCalc';
import { useUIStore } from '@/store/uiStore';
Comment on lines +51 to +53
// 2. Update physical entity position so it visibly moves!
entity.position = new Cesium.ConstantPositionProperty(p0);

Comment on lines +24 to +32
// Handle transitioning to FREE mode
if (mode === 'FREE' || !targetId) {
if (lastMode.current !== 'FREE') {
lastMode.current = 'FREE';
// Release any overrides if needed, but Cesium camera allows manual control natively
// when we stop overriding it in preRender.
}
return;
}
Comment on lines +258 to 263
<button
onClick={toggleFlybyHistory}
className={`relative transition-ui cursor-pointer p-2 min-w-[44px] min-h-[44px] flex items-center justify-center ${isFlybyHistoryOpen ? 'text-primary-container drop-shadow-[0_0_8px_rgba(0,229,255,0.6)]' : 'text-primary hover:text-primary-fixed'}`}
>
<MaterialIcon name="radar" />
</button>
Comment on lines +51 to +56
<button
onClick={toggleHistory}
className="text-on-surface-variant hover:text-primary-container transition-ui"
>
<MaterialIcon name="close" />
</button>
@krishkhinchi

krishkhinchi commented Aug 8, 2026

Copy link
Copy Markdown
Member

Hello @SohammPawarr, Please attach screenshots of the updates/changes along with your PR. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Artificial Intelligence and Machine Learning backend Backend development database Database and schema related changes documentation Improvements or additions to documentation enhancement New feature or request frontend Frontend development size/XL Very large or complex contribution. size:XL This PR changes 500-999 lines, ignoring generated files testing Tests added or improved type:backend Changes backend services or server-side logic. type:documentation Improves project documentation. type:feature Introduces a new feature or enhancement. type:frontend Changes frontend or client-side code. type:testing Adds or updates automated tests.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants