Skip to content

feat: add real-time satellite flyby notifications - #175

Open
SohammPawarr wants to merge 1 commit into
7-Blocks:mainfrom
SohammPawarr:feature/satellite-flyby-notifications
Open

feat: add real-time satellite flyby notifications#175
SohammPawarr wants to merge 1 commit into
7-Blocks:mainfrom
SohammPawarr:feature/satellite-flyby-notifications

Conversation

@SohammPawarr

@SohammPawarr SohammPawarr commented Aug 8, 2026

Copy link
Copy Markdown

This PR introduces a real-time, Mission Control-style notification system that tracks active satellites and alerts users whenever a selected satellite is about to pass over their current location or any of their bookmarked cities.

Changes Made
Orbital Calculation Engine (src/utils/orbitCalc.ts): Extracted the keplerToLatLonAlt logic from EarthTwin.tsx into a reusable utility. Added new Haversine distance and Max Elevation Angle calculations.
Background Flyby Hook (src/hooks/useFlybyEngine.ts): Implemented a lightweight polling interval that runs globally in the background. It propagates selected satellite orbits up to 60 minutes into the future to calculate the Time of Closest Approach (ETA) and visibility.
State Management (src/store/notificationStore.ts): Created a Zustand store to handle active alerts, history logging, and user preferences (warning time window, audio toggles).
Notification UI (src/components/ui/NotificationCenter.tsx & FlybyNotification.tsx):
Designed an immersive, telemetry-style toast notification.
Implemented a "History & Settings" slide-out panel accessible from the top command bar.
Built a synthesized double-beep audio alert using the Web Audio API for critical flyby detection.
Related Issue
Fixes #169

Testing Details
Verified the background interval logic triggers without blocking the main UI thread.
Tested orbital math predictions to ensure accurate Max Elevation and ETA metrics.
Confirmed that verbatim module imports for TS interfaces (like SpaceObject and FlybyNotification) are correctly typed to pass Vite's verbatimModuleSyntax strict checks.
Tested the rendering of the Notification Center and History Panel inside MainLayout.tsx.

Copilot AI lite review requested due to automatic review settings August 8, 2026 09:02
@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 bad376a Aug 08, 2026 · 09:02 09:05

@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 bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request frontend Frontend development size/XL Very large or complex contribution. type:bug Fixes an existing bug or unexpected behavior. type:documentation Improves project documentation. type:feature Introduces a new feature or enhancement. type:frontend Changes frontend or client-side code. labels Aug 8, 2026
@codeant-ai codeant-ai Bot added the size:XL This PR changes 500-999 lines, ignoring generated files label Aug 8, 2026
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds periodic satellite flyby detection, typed notification state, animated alerts, notification history, audio preferences, tracking actions, and shared orbital calculation utilities. It also wires flyby history into MainLayout and removes a deprecated TypeScript option.

Changes

Flyby notification flow

Layer / File(s) Summary
Shared orbital calculations
frontend/src/utils/orbitCalc.ts, frontend/src/components/EarthTwin.tsx
Adds shared orbital position, ground-distance, and elevation calculations. EarthTwin uses the shared orbital conversion utility.
Notification state and flyby detection
frontend/src/store/notificationStore.ts, frontend/src/hooks/useFlybyEngine.ts
Adds typed notification preferences and state. The flyby engine checks selected satellites, calculates visible passes, and creates qualifying notifications every 30 seconds.
Notification display and layout integration
frontend/src/components/ui/FlybyNotification.tsx, frontend/src/components/ui/NotificationCenter.tsx, frontend/src/components/layouts/MainLayout.tsx, frontend/src/store/uiStore.ts, frontend/tsconfig.app.json
Adds animated alerts, browser notifications, audio feedback, history controls, tracking navigation, and flyby-history state. Removes ignoreDeprecations from the TypeScript configuration.

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

Sequence Diagram(s)

sequenceDiagram
  participant MainLayout
  participant NotificationCenter
  participant useFlybyEngine
  participant SatelliteCatalog
  participant useNotificationStore
  participant FlybyNotification
  MainLayout->>NotificationCenter: render notification center
  NotificationCenter->>useFlybyEngine: start periodic flyby checks
  useFlybyEngine->>SatelliteCatalog: fetch selected satellite data
  useFlybyEngine->>useNotificationStore: create qualifying notification
  NotificationCenter->>FlybyNotification: render active alert
  FlybyNotification->>useNotificationStore: dismiss or track alert
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the feature, links issue #169, and lists testing, but it omits required template sections and the mandatory single ECSoC26 difficulty selection. Add the missing template sections, complete the required checkboxes, and select exactly one ECSoC26 difficulty level.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly and concisely describes the main change: adding real-time satellite flyby notifications.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 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.

@github-actions github-actions Bot added the AI Artificial Intelligence and Machine Learning label Aug 8, 2026
Comment on lines +18 to +20
const ctx = new AudioContext();
const osc = ctx.createOscillator();
const gainNode = ctx.createGain();

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: Each alert creates a new AudioContext, but the context is never closed after the oscillator stops. Repeated flyby alerts or preference-triggered effect reruns therefore retain audio contexts and can eventually hit browser audio-resource limits; close the context after playback completes. [resource leak]

Severity Level: Major ⚠️
- ⚠️ Long-running sessions accumulate audio contexts.
- ⚠️ Repeated flybys increase browser audio-resource usage.
- ⚠️ Excessive alerts may eventually prevent further sound playback.

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:** 18:20
**Comment:**
	*Resource Leak: Each alert creates a new `AudioContext`, but the context is never closed after the oscillator stops. Repeated flyby alerts or preference-triggered effect reruns therefore retain audio contexts and can eventually hit browser audio-resource limits; close the context after playback completes.

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 +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.

Suggestion: The effect depends on the entire notification object and preferences.soundEnabled, so changing the audio preference reruns it for every currently active toast. This replays browser notifications for old alerts and can replay their sounds when audio is enabled; trigger the alert side effects only when a notification is newly created, using a stable notification identifier. [logic error]

Severity Level: Major ⚠️
- ⚠️ Toggling Audio Alerts repeats sounds for active flyby toasts.
- ⚠️ Existing alerts can produce duplicate browser notifications.
- ⚠️ Preference changes cause unwanted alert side effects.

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:** 45:59
**Comment:**
	*Logic Error: The effect depends on the entire `notification` object and `preferences.soundEnabled`, so changing the audio preference reruns it for every currently active toast. This replays browser notifications for old alerts and can replay their sounds when audio is enabled; trigger the alert side effects only when a notification is newly created, using a stable notification identifier.

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: The engine owns a separate useBookmarkStorage() state instance from the one used by EarthTwin through useBookmarks(). Bookmark changes made in the globe therefore update local storage but do not update this engine's bookmarks state, so newly added, edited, or deleted locations are not reflected in flyby checks until the engine is remounted. [stale reference]

Severity Level: Major ⚠️
- ⚠️ Bookmark-based flyby targets remain stale.
- ⚠️ Added locations are omitted from notification scans.
- ⚠️ Deleted locations may continue receiving checks.

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: The engine owns a separate `useBookmarkStorage()` state instance from the one used by `EarthTwin` through `useBookmarks()`. Bookmark changes made in the globe therefore update local storage but do not update this engine's `bookmarks` state, so newly added, edited, or deleted locations are not reflected in flyby checks until the engine 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
👍 | 👎

@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: 7

🤖 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 `@frontend/src/components/ui/FlybyNotification.tsx`:
- Around line 18-35: Update playBeep to reuse a single persistent AudioContext
stored in module state instead of constructing one per alert; initialize it
lazily when needed, and create each oscillator and gain node from that context
while preserving the existing beep timing and warning behavior.
- Around line 45-59: Update the FlybyNotification delivery logic in the
useEffect so browser alerts are emitted only once per notification.id, even when
preferences.soundEnabled changes or the component re-renders. Track delivered
IDs with a suitable persistent set or split the sound effect from the
browser-notification effect, ensuring sound preference changes only affect
playback and do not recreate browser notifications.
- Line 67: Update FlybyNotification’s ETA calculation to use a current timestamp
state rather than only the render-time Date.now() value. Add minute-based
updates while the toast remains visible, clean up the timer when dismissed or
unmounted, and derive minutesAway from that tracked timestamp so the displayed
countdown refreshes each minute.

In `@frontend/src/hooks/useFlybyEngine.ts`:
- Around line 41-48: Update the checkFlybys flow in useFlybyEngine’s useEffect
to track whether the effect is still active, and have the async response
handling return before adding notifications when cleanup has run. Set that
inactive state in the effect cleanup alongside clearing the interval, preserving
notification behavior for requests belonging to the current tracked-satellite
selection.
- Around line 31-37: Update the geolocation failure callback and the
non-geolocation branch in the surrounding hook to leave userLocationRef.current
null instead of assigning the fallback coordinates (0, 0). Preserve the
successful geolocation assignment so flyby alerts are created only after a real
location is available.

In `@frontend/src/store/notificationStore.ts`:
- Around line 39-43: Update the duplicate check in the notification store’s
isDuplicate logic to remove the !n.dismissed condition. Duplicate suppression
must compare satelliteId, locationName, and the five-minute ETA window
regardless of dismissal state, while dismissed remains display-only.

In `@frontend/src/utils/orbitCalc.ts`:
- Around line 20-23: Validate the epoch in the orbit propagation logic before
calculating elapsedDays: when an explicitly provided obj.epoch produces an
invalid Date, return the function’s established invalid-input result rather than
propagating NaN values. Keep the default current-date behavior for a missing
epoch and preserve normal propagation for valid epochs.
🪄 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: 4fabf9f3-a4d9-445d-b035-3bd70b1fe25c

📥 Commits

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

📒 Files selected for processing (9)
  • frontend/src/components/EarthTwin.tsx
  • frontend/src/components/layouts/MainLayout.tsx
  • frontend/src/components/ui/FlybyNotification.tsx
  • frontend/src/components/ui/NotificationCenter.tsx
  • 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 on lines +18 to +35
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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate file:"
fd -a 'FlybyNotification\.tsx$' . || true

echo
echo "File contents/outline:"
file="$(fd 'FlybyNotification\.tsx$' . | head -n1 || true)"
if [ -n "${file:-}" ]; then
  wc -l "$file"
  ast-grep outline "$file" --view compact || true
  echo
  cat -n "$file"
fi

echo
echo "Search for playBeep/usages:"
rg -n "playBeep|AudioContext|FlybyNotification" frontend src . 2>/dev/null | head -n 200 || true

Repository: 7-Blocks/Kepler

Length of output: 9976


🌐 Web query:

MDN AudioContext close recommended practice after use

💡 Result:

The recommended practice for using AudioContext is to create a single instance and reuse it throughout your application's lifecycle, rather than creating and closing it frequently [1]. When you no longer need an AudioContext, calling the close method is the standard way to release system audio resources, such as those that might block the creation of additional contexts or impact CPU/battery usage [2][1][3]. Key considerations regarding AudioContext.close: 1. Purpose: The primary purpose of close is to forcibly release system audio resources, stop audio processing, and suspend the progression of audio time [2][1]. 2. Resource Management: Calling close does not automatically release all objects created by the AudioContext; it specifically targets system audio resources [2][3]. Garbage collection will eventually handle the memory associated with the context and its nodes once you release your references to them (e.g., by setting the variable to null) [4]. 3. Promise-based: The close method returns a Promise [2]. It is good practice to wait for this promise to resolve if you need to ensure all blocking resources are released before proceeding (for example, before attempting to initialize a new context) [2][5]. 4. Alternatives for Temporary Pauses: If you only need to temporarily halt audio output or reduce CPU/battery consumption without destroying the context entirely, consider using the suspend method instead of close [1]. In summary, use close when you are finished with the audio functionality for an extended period or the application lifecycle ends [2][1]. For active applications, prefer reusing a single instance and using suspend/resume to manage temporary interruptions or state changes [6][1][7].

Citations:


🌐 Web query:

Web Audio API AudioContext context limits number of active contexts browser

💡 Result:

The Web Audio API specification does not mandate a specific maximum number of active AudioContext instances [1][2]. Instead, it allows browser implementations to define their own limits [2]. When this limit is exceeded, an implementation may throw a NotSupportedError when attempting to create a new AudioContext [2]. Because limits are implementation-defined, they vary across browsers and platforms: - Browser-Specific and Platform-Specific Limits: Browsers may impose different caps based on the underlying OS or hardware [3][2]. For instance, source code for the WebKit engine (used by Safari) has historically included platform-specific constraints, such as a limit of four hardware contexts on Windows [3]. - Historical Context in Chrome: Older versions of Google Chrome (prior to version 66) enforced a limit of six AudioContext instances per tab [1][4]. - Best Practices: It is widely recommended to create a single AudioContext and reuse it throughout your application rather than initializing multiple instances [5]. This is both more resource-efficient and avoids potential issues with browser-imposed limits or performance bottlenecks [5]. If your application requires multiple audio graphs, you should manage them within a single AudioContext or use OfflineAudioContext for non-real-time rendering tasks where applicable [5].

Citations:


Reuse a single AudioContext instead of creating one per alert.

playBeep creates a new AudioContext for every notification while never closing or reusing it. Keep one persistent context in module state and have each beep start/stop on that context to avoid context limits and resource buildup.

🤖 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 18 - 35,
Update playBeep to reuse a single persistent AudioContext stored in module state
instead of constructing one per alert; initialize it lazily when needed, and
create each oscillator and gain node from that context while preserving the
existing beep timing and warning behavior.

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate FlybyNotification.tsx"
fd -a 'FlybyNotification\.tsx$' . || true

echo
echo "Show relevant file outline and lines"
file="$(fd 'FlybyNotification\.tsx$' . | head -n 1)"
if [ -n "$file" ]; then
  echo "FILE=$file"
  wc -l "$file"
  cat -n "$file"
fi

echo
echo "Search notification data shape and prefs usage"
rg -n "soundEnabled|FlybyNotification|notification/id|id:" -S .

Repository: 7-Blocks/Kepler

Length of output: 30719


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Show notification store"
cat -n frontend/src/store/notificationStore.ts

echo
echo "Show NotificationCenter component"
cat -n frontend/src/components/ui/NotificationCenter.tsx

echo
echo "Static verifier: FlybyNotification effect dependencies and delivery side effects"
python3 - <<'PY'
from pathlib import Path
import re

p = Path('frontend/src/components/ui/FlybyNotification.tsx')
text = p.read_text()

m = re.search(r"useEffect\s*\(\s*\(\)\s*=>\s*\{(.*?)\n\s*\}\s*,\s*\[(.*?)\]\s*\);", text, re.S)
if not m:
    print("NO_DETECTED_EFFECT")
    raise SystemExit(1)

body = m.group(1)
deps = [d.strip() for d in m.group(2).split(',') if d.strip()]
print("deps=", deps)
print("has_sound_effect=", "preferences.soundEnabled" in body)
print("has_create_notification=", "new Notification(" in body)
print("has_request_permission=", "Notification.requestPermission(" in body)

uses_notification_id = bool(re.search(r"notification\.id", body))
uses_sound_enabled = bool(re.search(r"preferences\.soundEnabled", body))
uses_notification_name_or_eta = bool(re.search(r"notification\.(satelliteName|locationName|eta)", body))

print("uses_notification_id_in_effect=", uses_notification_id)
print("uses_sound_enabled_in_effect=", uses_sound_enabled)
print("uses_notification_field_in_effect=", uses_notification_name_or_eta)
print("issue_sound_dependency_retriggers=", uses_sound_enabled)
print("issue_notification_creates_on_each_retrieval=", uses_notification_id is False and uses_notification_name_or_eta)
PY

Repository: 7-Blocks/Kepler

Length of output: 10860


Deliver alert alerts only on first appearance.

preferences.soundEnabled changes the dependency for every active FlybyNotification, so changing audio settings re-runs delivery for dismissed/old toasts. Track notification.id in a delivery set and create only the browser notification, or split the sound and browser-notification effects so audio changes do not create new browser 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/components/ui/FlybyNotification.tsx` around lines 45 - 59,
Update the FlybyNotification delivery logic in the useEffect so browser alerts
are emitted only once per notification.id, even when preferences.soundEnabled
changes or the component re-renders. Track delivered IDs with a suitable
persistent set or split the sound effect from the browser-notification effect,
ensuring sound preference changes only affect playback and do not recreate
browser notifications.

dismiss(notification.id);
};

const minutesAway = Math.max(0, Math.round((notification.eta.getTime() - Date.now()) / 60000));

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

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching FlybyNotification:"
fd -a 'FlybyNotification\.tsx$' . || true

file="$(fd 'FlybyNotification\.tsx$' . | head -n1 || true)"
if [ -n "${file:-}" ]; then
  echo
  echo "Line count:"
  wc -l "$file"
  echo
  echo "File contents with line numbers:"
  cat -n "$file"
fi

echo
echo "Search for FlybyNotification usages:"
rg -n "FlybyNotification|eta|minutesAway" . --glob '!node_modules' --glob '!dist' --glob '!build' || true

Repository: 7-Blocks/Kepler

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Matching files:"
fd -a 'FlybyNotification\.tsx$|FlybyNotification\.ts$' . || true

echo
echo "Search focused usages:"
rg -n "FlybyNotification|minutesAway|Date\.now\(\)|eta\.getTime\(\)|T-\{minutesAway MIN\}\"" . \
  --glob '!node_modules' --glob '!build' --glob '!dist' \
  --glob '!*.svg' --glob '!*.png' --glob '!*.jpg' --glob '!*.jpeg' \
  --glob '!*.webp' --glob '!*.gif' --glob '!*.ico' --glob '!*.woff*' || true

Repository: 7-Blocks/Kepler

Length of output: 3899


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="$(fd 'FlybyNotification\.tsx$' . | head -n1 || true)"
echo "FlybyNotification file: $file"
wc -l "$file"
echo
sed -n '1,140p' "$file" | cat -n

echo
echo "NotificationStore relevant lines:"
sed -n '1,120p' frontend/src/store/notificationStore.ts | cat -n

echo
echo "NotificationCenter relevant lines:"
sed -n '1,80p' frontend/src/components/ui/NotificationCenter.tsx | cat -n

Repository: 7-Blocks/Kepler

Length of output: 14011


🌐 Web query:

React Framer Motion AnimatePresence motion.div timer interval state cleanup clear on unmount

💡 Result:

To correctly handle timer or interval cleanup within a component being unmounted by AnimatePresence, you must leverage the useEffect cleanup function in combination with Framer Motion's usePresence hook [1][2][3]. When a component is removed from the React tree but wrapped in AnimatePresence, it remains mounted in the DOM while its exit animation runs [4][5]. Standard useEffect cleanup functions will run as soon as the component is removed from the React tree, which may stop your timers prematurely or lead to errors if you attempt to update state on an unmounted component [6][7]. To manage this properly: 1. Use the usePresence hook to track the component's exit status [2][3]. 2. Create your timer or interval inside a useEffect that depends on the isPresent state returned by usePresence [2][3]. 3. If you need to manually control when the component is finally removed (e.g., waiting for a timer to finish alongside an animation), use the safeToRemove callback returned by usePresence [1][2][3]. Example pattern for delayed removal with a timer: import { useEffect } from 'react'; import { usePresence, motion } from 'motion/react'; function DelayedComponent { const [isPresent, safeToRemove] = usePresence; useEffect( => { if (!isPresent) { // Logic to run during exit phase (e.g., wait 1s) const timer = setTimeout(safeToRemove, 1000); // Cleanup the timer if the component is somehow // forced to unmount before the timer finishes return => clearTimeout(timer); } }, [isPresent, safeToRemove]); return <motion.div exit={{ opacity: 0 }}>Content</motion.div>; } By including a cleanup function (return => clearTimeout(timer)), you ensure that no memory leaks or errors occur if the exit state changes rapidly [6]. If your goal is simply to stop an interval from trying to update state on an unmounted component, the standard useEffect cleanup is sufficient; however, if you are coordinating custom exit timing, usePresence and safeToRemove are the authoritative tools [1][2][7].

Citations:


Update the ETA each minute.

Date.now() is cached in render, so T-{minutesAway} MIN stays fixed while the toast is rendered with no other FlybyNotification state changes. Track a current timestamp state and update it until the toast is dismissed.

🤖 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` at line 67, Update
FlybyNotification’s ETA calculation to use a current timestamp state rather than
only the render-time Date.now() value. Add minute-based updates while the toast
remains visible, clean up the timer when dismissed or unmounted, and derive
minutesAway from that tracked timestamp so the displayed countdown refreshes
each minute.

Comment on lines +31 to +37
(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 use (0, 0) as a location fallback.

When geolocation fails, this code adds Null Island as a notification target. The engine then creates flyby alerts for an unrelated location. Keep userLocationRef.current null until location access succeeds.

Proposed fix
         (error) => {
-          console.warn('Geolocation denied or failed, using default location (0,0).', error);
-          userLocationRef.current = { lat: 0, lon: 0 };
+          console.warn('Geolocation denied or failed. Current-location flyby alerts are disabled.', 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) => {
console.warn('Geolocation denied or failed. Current-location flyby alerts are disabled.', 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 - 37, Update the
geolocation failure callback and the non-geolocation branch in the surrounding
hook to leave userLocationRef.current null instead of assigning the fallback
coordinates (0, 0). Preserve the successful geolocation assignment so flyby
alerts are created only after a real location is available.

Comment on lines +41 to +48
useEffect(() => {
if (selectedSatelliteIds.length === 0) return;

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

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

Stop stale checks from adding notifications.

Cleanup clears the interval but does not stop checkFlybys calls already awaiting API responses. If tracked satellites change or the component unmounts during a request, the old call can add notifications for stale satellite IDs.

Proposed fix
   useEffect(() => {
     if (selectedSatelliteIds.length === 0) return;
+    let isCurrent = true;
 
     const checkFlybys = async () => {
       try {
         // Fetch data for all tracked satellites
         const satPromises = selectedSatelliteIds.map(id => api.getCatalogObjectByNorad(id));
         const responses = await Promise.allSettled(satPromises);
+        if (!isCurrent) return;
 
         const satellites = responses
@@
-    return () => clearInterval(intervalId);
+    return () => {
+      isCurrent = false;
+      clearInterval(intervalId);
+    };
   }, [selectedSatelliteIds, bookmarks, preferences.warningMinutes, addNotification]);

Also applies to: 124-128

🤖 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 41 - 48, Update the
checkFlybys flow in useFlybyEngine’s useEffect to track whether the effect is
still active, and have the async response handling return before adding
notifications when cleanup has run. Set that inactive state in the effect
cleanup alongside clearing the interval, preserving notification behavior for
requests belonging to the current tracked-satellite selection.

Comment on lines +39 to +43
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

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

Keep dismissed notifications in duplicate suppression.

Line 42 excludes dismissed records. After a user dismisses an alert, the periodic flyby engine can add the same flyby again while its ETA remains inside the five-minute window. Dismissal then fails to suppress the alert.

Remove the dismissed-state condition from this identity check. Keep dismissed only for display state.

Proposed fix
       (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
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
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
🤖 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 39 - 43, Update the
duplicate check in the notification store’s isDuplicate logic to remove the
!n.dismissed condition. Duplicate suppression must compare satelliteId,
locationName, and the five-minute ETA window regardless of dismissal state,
while dismissed remains display-only.

Comment on lines +20 to +23
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;

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

Reject invalid epochs before propagation.

If obj.epoch is malformed, new Date(obj.epoch).getTime() is NaN. The function then returns non-null lat and lon values of NaN, which violates its invalid-input contract and corrupts downstream rendering and flyby calculations.

Proposed fix
-  const epochDate = obj.epoch ? new Date(obj.epoch) : new Date();
   const now = new Date();
-  const elapsedDays = (now.getTime() - epochDate.getTime()) / 86400000 + (timeOffsetSec / 86400);
+  const epochMs = obj.epoch ? new Date(obj.epoch).getTime() : now.getTime();
+  if (!Number.isFinite(epochMs)) return null;
+
+  const elapsedDays = (now.getTime() - epochMs) / 86400000 + (timeOffsetSec / 86400);
📝 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 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 now = new Date();
const epochMs = obj.epoch ? new Date(obj.epoch).getTime() : now.getTime();
if (!Number.isFinite(epochMs)) return null;
const elapsedDays = (now.getTime() - epochMs) / 86400000 + (timeOffsetSec / 86400);
const currentMeanAnomaly = ((obj.mean_anomaly + (elapsedDays * obj.mean_motion * 360)) % 360) * Math.PI / 180;
🤖 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 20 - 23, Validate the epoch in
the orbit propagation logic before calculating elapsedDays: when an explicitly
provided obj.epoch produces an invalid Date, return the function’s established
invalid-input result rather than propagating NaN values. Keep the default
current-date behavior for a missing epoch and preserve normal propagation for
valid epochs.

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 introduces a client-side “flyby alert” system in the frontend: it periodically evaluates upcoming satellite passes for the user’s current location and saved bookmarks, generates toast notifications, and provides a history/settings panel for managing alert behavior.

Changes:

  • Added orbital math utilities and a polling “flyby engine” hook to predict visible passes and trigger alerts.
  • Implemented notification state management (alerts + preferences) and new UI components for toasts and alert history/settings.
  • Integrated the notification center into the main app layout and refactored EarthTwin to reuse the shared orbit calculation utility.

Reviewed changes

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

Show a summary per file
File Description
frontend/tsconfig.app.json Removes a TypeScript deprecation-suppression setting.
frontend/src/utils/orbitCalc.ts Adds shared orbit propagation + distance/elevation helpers for flyby prediction.
frontend/src/store/uiStore.ts Adds UI state + toggle for opening the flyby history panel.
frontend/src/store/notificationStore.ts Introduces zustand store for flyby notifications and user preferences.
frontend/src/hooks/useFlybyEngine.ts Adds the periodic flyby detection/prediction loop and notification generation.
frontend/src/components/ui/NotificationCenter.tsx Adds toast display plus flyby history/settings panel UI.
frontend/src/components/ui/FlybyNotification.tsx Adds the individual flyby toast UI + audio/browser notification behaviors.
frontend/src/components/layouts/MainLayout.tsx Mounts the notification center globally and adds a top-bar toggle button.
frontend/src/components/EarthTwin.tsx Refactors to import keplerToLatLonAlt from the new shared utility.
Suppressed comments (1)

frontend/src/components/ui/NotificationCenter.tsx:70

  • This icon-only toggle button needs an aria-label so assistive tech can announce what the control does (enable/disable audio alerts).
                  <button 
                    onClick={() => updatePreferences({ soundEnabled: !preferences.soundEnabled })}
                    className={`text-lg transition-ui ${preferences.soundEnabled ? 'text-primary-container' : 'text-on-surface-variant'}`}
                  >
                    <MaterialIcon name={preferences.soundEnabled ? 'volume_up' : 'volume_off'} />

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

Comment on lines +77 to +90
// 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);
Comment on lines +41 to +49
useEffect(() => {
if (selectedSatelliteIds.length === 0) return;

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

Comment on lines +118 to +121
}
} catch (err) {
console.error('Flyby engine error:', err);
}
Comment on lines +18 to +33
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);
Comment on lines +50 to +58
// 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();
}
Comment on lines +51 to +56
<button
onClick={toggleHistory}
className="text-on-surface-variant hover:text-primary-container transition-ui"
>
<MaterialIcon name="close" />
</button>
Comment on lines +85 to +90
<button
onClick={() => dismiss(notification.id)}
className="text-on-surface-variant hover:text-primary-container transition-ui"
>
<MaterialIcon name="close" className="text-sm" />
</button>
@github-actions github-actions Bot added backend Backend development database Database and schema related changes testing Tests added or improved type:backend Changes backend services or server-side logic. type:testing Adds or updates automated tests. labels Aug 8, 2026
@SohammPawarr
SohammPawarr force-pushed the feature/satellite-flyby-notifications branch from 5ee1046 to bad376a Compare August 8, 2026 10:08
@krishkhinchi

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 bug Something isn't working 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:bug Fixes an existing bug or unexpected behavior. 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.

Satellite Flyby Notification System

3 participants