Skip to content

Add animated dashboard counters (closes #88)#93

Open
SankeerthNara wants to merge 5 commits into
7-Blocks:mainfrom
SankeerthNara:Animated-Dashboard-Counters
Open

Add animated dashboard counters (closes #88)#93
SankeerthNara wants to merge 5 commits into
7-Blocks:mainfrom
SankeerthNara:Animated-Dashboard-Counters

Conversation

@SankeerthNara

@SankeerthNara SankeerthNara commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

User description

Description

Adds a reusable AnimatedCounter component for Kepler's dashboard KPI stats
(Tracked Satellites, Debris Objects, Active Alerts, Pred. Collisions, Space
Weather, Agent Runs).

  • useAnimatedCounter — animation logic only (rAF + easing). Animates from 0
    on first load, from the previous value on updates, skips re-animating on an
    unchanged value, respects prefers-reduced-motion, duration clamped to
    800–1500ms.
  • numberFormatter — pure number formatting (grouping, decimals, compact
    notation, prefix/suffix), no React dependency.
  • AnimatedCounter / StatisticCard — presentational components. Includes a
    loading skeleton and screen-reader support (only the settled final value is
    announced, not each animation frame).
  • Wired into Dashboard.tsx's KPI grid.
  • 13 unit tests (Vitest) covering formatting edge cases and animation
    start-from-0 / start-from-previous / no-op / reduced-motion behavior.

While getting a clean build, I also fixed two small pre-existing issues that
were blocking npm run build and unrelated to the counter feature itself:

  • A duplicate Hero import in LandingPage.tsx colliding with a local
    Hero component already defined in the same file
  • Two broken Tailwind @apply classes in index.css referencing undefined
    theme colors (text-foreground, border-border)

Related Issue

Fixes #88

Checklist

  • I have read the contributing guidelines
  • My code follows the project guidelines
  • I have completed testing of my changes
  • Documentation has been updated (if applicable)

Screenshots / Screen Recordings

Recording.2026-07-20.104740.mp4

Breaking Changes

None.

Summary by CodeRabbit

  • New Features
    • Added reusable animated numeric counters (configurable duration, loading skeletons, highlight-on-increase).
    • Added dashboard statistic cards featuring icons, labels, and animated values.
    • Enhanced KPI rendering to use numeric values with optional fallback text.
  • Accessibility & Styling
    • Improved screen-reader formatting for counters and respected reduced-motion preferences.
    • Added counter shimmer/highlight styling and refined base page styles.
  • Tests & Tooling
    • Introduced Vitest setup plus unit tests for the counter hook and number formatting; added a test script.
  • Chores
    • Removed the backend database seeding script and its tests.

CodeAnt-AI Description

Add animated dashboard counters with cleaner number display

What Changed

  • Dashboard KPI values now animate when they load or update instead of appearing all at once
  • Zero-value KPIs still show clear fallback text like and IDLE instead of animating a number
  • Large and decimal values are formatted consistently, with support for prefixes, suffixes, compact display, and readable screen-reader output
  • Loading states now show a skeleton placeholder, and reduced-motion users skip the animation
  • Fixed build issues caused by a duplicate landing page import and invalid global styles

Impact

✅ Clearer dashboard loading
✅ Smoother KPI updates
✅ Readable values for accessibility

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

Greptile Summary

This PR adds an AnimatedCounter component (backed by a useAnimatedCounter hook and a numberFormatter utility) that animates KPI numbers on the dashboard from 0 on first load and from the previous value on updates, with full reduced-motion and screen-reader support. It also fixes two pre-existing build blockers in index.css and a duplicate import in LandingPage.tsx.

  • useAnimatedCounter drives animation via requestAnimationFrame with an easeOutExpo curve; duration is clamped to 800–1500 ms, prefers-reduced-motion is respected with both a JS check and a CSS fallback, and decimal places are pinned to the final target value (not the intermediate display value) to avoid mid-animation flicker.
  • AnimatedCounter / numberFormatter cover formatting (grouping separators, compact notation, prefix/suffix, exponential edge cases) with a module-level Intl.NumberFormat cache and 13 Vitest unit tests.
  • Dashboard.tsx wires all six KPI tiles to AnimatedCounter, using static fallback strings ('—', 'IDLE') for zero-value states that should not display "0".

Confidence Score: 5/5

Safe to merge — the changes are additive UI components with no backend impact, the pre-existing build fixes are minimal and correct, and the new animation path is well-tested.

The core animation logic, formatting utility, and dashboard wiring are all well-implemented and covered by unit tests. The only open item is a newly-created StatisticCard component that is not yet wired into the dashboard (dead code), which has no runtime impact.

frontend/src/components/StatisticCard.tsx — created but not imported anywhere; worth confirming whether it should replace the inline AnimatedCounter usage in Dashboard or is intentionally reserved for future use.

Important Files Changed

Filename Overview
frontend/src/hooks/useAnimatedCounter.ts New hook implementing rAF-based counter animation with easeOutExpo, prefers-reduced-motion support, duration clamping, and render-time state sync for skip-animation path. Logic is sound.
frontend/src/components/AnimatedCounter.tsx New presentational counter component; correctly pins decimals to the final target value (not the intermediate displayValue), avoiding decimal-flicker. aria-live pattern is correct (text swapped in/out on a fixed polite region).
frontend/src/utils/numberFormatter.ts Pure formatting utility with formatter cache; correctly handles exponential-notation edge cases and caps decimals.
frontend/src/components/StatisticCard.tsx New card component wrapping AnimatedCounter with icon/label layout; well-structured but never imported anywhere — Dashboard.tsx embeds AnimatedCounter directly in its own motion.div template instead.
frontend/src/pages/Dashboard.tsx KPI grid wired to AnimatedCounter; fallback strings ('—', 'IDLE') correctly bypass the counter for zero-value states. Removed unused toast import.
frontend/src/styles/index.css Fixed two broken @apply directives referencing undefined Tailwind theme colors, replacing them with hardcoded hex equivalents to fix the build.
frontend/src/hooks/tests/useAnimatedCounter.test.ts 7 tests covering first-render from-0, no-op on unchanged value, from-previous-value on update, reduced-motion skip, duration clamping, mid-flight target change, and missing matchMedia.
frontend/src/utils/tests/numberFormatter.test.ts 6 tests covering grouping separators, decimal inference, suffix/prefix, compact notation, explicit decimals override, exponential edge case, and grouping disable.
frontend/vitest.config.ts Minimal vitest config with jsdom environment and globals enabled; tests use relative imports only so no path alias issues.
frontend/src/components/animated-counter.css Defines shimmer keyframes and reduced-motion media query override. Correctly imported in main.tsx.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["AnimatedCounter receives value prop"] --> B{"loading?"}
    B -- yes --> C["Render shimmer skeleton\n(aria-hidden, CSS shimmer)"]
    B -- no --> D["useAnimatedCounter(value, options)"]
    D --> E{"shouldSkipAnimation?\n(disableAnimation || prefersReducedMotion)"}
    E -- yes --> F["Render-time sync:\nanimatedValue = target instantly"]
    E -- no --> G{"target changed\nsince last render?"}
    G -- no --> H["No-op — isAnimating stays false"]
    G -- yes --> I["Start rAF loop from\nlast visual position (fromRef)"]
    I --> J["Tick: easeOutExpo(elapsed / duration)\n→ setAnimatedValue(next)"]
    J --> K{"progress < 1?"}
    K -- yes --> J
    K -- no --> L["setAnimatedValue(target)\nisAnimating = false"]
    F --> M["inferDecimals(target) → decimals\nformatNumber(displayValue, decimals)"]
    H --> M
    L --> M
    M --> N["Visual span (aria-hidden): animated text"]
    M --> O["SR-only span (aria-live=polite):\nempty while animating → final text on settle"]
Loading

Reviews (5): Last reviewed commit: "Fixes" | Re-trigger Greptile

@codeant-ai

codeant-ai Bot commented Jul 20, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Incremental review completed c85da10 Jul 22, 2026 · 13:33 13:34
✅ Incremental review completed 25516a8 Jul 20, 2026 · 11:28 11:29
✅ Reviewed your PR 20a3eb8 Jul 20, 2026 · 05:19 05:23

Updated in place by CodeAnt AI · last 5 reviews

@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/L Large contribution requiring significant work. size:L Large size 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 Jul 20, 2026
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds reusable locale-aware number formatting, animated counters with loading and reduced-motion handling, statistic cards, dashboard KPI integration, accessibility behavior, Vitest/jsdom tooling, counter styling, and frontend support updates.

Changes

Animated counter feature

Layer / File(s) Summary
Formatting contract and utilities
frontend/src/utils/numberFormatter.ts, frontend/src/utils/__tests__/numberFormatter.test.ts
Adds configurable locale-aware formatting, compact notation, prefixes/suffixes, precision inference, caching, and non-compact screen-reader output with tests.
Counter animation hook
frontend/src/hooks/useAnimatedCounter.ts, frontend/src/hooks/__tests__/useAnimatedCounter.test.ts
Adds eased requestAnimationFrame animation, reduced-motion support, duration bounds, retargeting, skip behavior, and cleanup with behavioral tests.
Counter and statistic card rendering
frontend/src/components/AnimatedCounter.tsx, frontend/src/components/StatisticCard.tsx, frontend/src/components/animated-counter.css, frontend/src/main.tsx
Adds animated counter and statistic card components with loading skeletons, increase highlighting, accessible rendering, and imported styles.
Dashboard KPI integration
frontend/src/pages/Dashboard.tsx
Passes numeric KPI values to AnimatedCounter while retaining fallback text for non-numeric states.
Frontend wiring and test tooling
frontend/package.json, frontend/vitest.config.ts, package.json, frontend/src/pages/LandingPage.tsx, frontend/src/styles/index.css, backend/.gitignore
Adds test configuration and dependencies, updates LandingPage imports, replaces selected Tailwind base rules with explicit CSS declarations, and adds Python artifact ignores.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Dashboard
  participant StatisticCard
  participant AnimatedCounter
  participant useAnimatedCounter
  Dashboard->>StatisticCard: provide numeric KPI value
  StatisticCard->>AnimatedCounter: pass value and formatting options
  AnimatedCounter->>useAnimatedCounter: request animated display value
  useAnimatedCounter-->>AnimatedCounter: return interpolated value and animation state
  AnimatedCounter-->>StatisticCard: render formatted accessible counter
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The backend seed script/test deletions and the added root package manifests are unrelated to the animated dashboard counter feature. Move backend cleanup and package-manifest changes into a separate PR, or document why they belong in this scope.
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The hook, reusable components, dashboard wiring, formatting, loading, reduced-motion, accessibility, and tests match issue #88.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding animated dashboard counters.
Description check ✅ Passed The description includes the main sections, issue link, checklist, screenshots, and breaking changes, with enough implementation detail.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@codeant-ai codeant-ai Bot added size:L Large size and removed size:L Large size labels Jul 20, 2026
@github-actions github-actions Bot added the AI Artificial Intelligence and Machine Learning label Jul 20, 2026
Comment on lines +27 to +32
useEffect(() => {
const mql = window.matchMedia('(prefers-reduced-motion: reduce)');
const listener = (e: MediaQueryListEvent) => setReduced(e.matches);
mql.addEventListener('change', listener);
return () => mql.removeEventListener('change', listener);
}, []);

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 effect assumes window.matchMedia exists and always supports addEventListener, which is not true in some runtimes (older browsers and certain test environments). Calling it unguarded will throw at runtime and crash the component; add capability checks and a fallback to legacy listener APIs before attaching listeners. [api mismatch]

Severity Level: Critical 🚨
- ❌ Dashboard KPI counters can crash in unsupported browsers.
- ⚠️ Vitest/jsdom runs may fail without matchMedia polyfill.
Steps of Reproduction ✅
1. When the dashboard renders, `AnimatedCounter` (at
`frontend/src/components/AnimatedCounter.tsx:27-38`) calls `useAnimatedCounter`, which in
turn calls `usePrefersReducedMotion` from
`frontend/src/hooks/useAnimatedCounter.ts:22-35`.

2. On mount, `usePrefersReducedMotion` first evaluates the state initializer at
`frontend/src/hooks/useAnimatedCounter.ts:23-25`, which calls
`window.matchMedia('(prefers-reduced-motion: reduce)').matches` whenever `typeof window
!== 'undefined'`. In a runtime where `window` exists but `window.matchMedia` is undefined
or not a function (older browsers, some test environments), this throws immediately and
breaks rendering.

3. The subsequent `useEffect` at `frontend/src/hooks/useAnimatedCounter.ts:27-32`
unconditionally does `const mql = window.matchMedia(...)` and then
`mql.addEventListener('change', listener)`. In environments where `matchMedia` exists but
only exposes the legacy `addListener`/`removeListener` API, `mql.addEventListener` is
undefined, so calling it raises a `TypeError` and crashes the component.

4. Because `AnimatedCounter` is used directly in the main dashboard KPI grid
(`frontend/src/pages/Dashboard.tsx:157-159`) without any feature detection around
reduced-motion or `matchMedia`, any runtime lacking full `window.matchMedia` and
`MediaQueryList.addEventListener` support will fail during the initial render of the
dashboard, preventing KPI counters from displaying.

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/useAnimatedCounter.ts
**Line:** 27:32
**Comment:**
	*Api Mismatch: This effect assumes `window.matchMedia` exists and always supports `addEventListener`, which is not true in some runtimes (older browsers and certain test environments). Calling it unguarded will throw at runtime and crash the component; add capability checks and a fallback to legacy listener APIs before attaching listeners.

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 +30 to +32
const str = value.toString();
const dot = str.indexOf('.');
if (dot === -1) return 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.

Suggestion: inferDecimals assumes decimal numbers always contain a . in toString(), but JavaScript renders very small/large floats in exponential form (for example 1e-7), so this path returns 0 decimals and formats non-zero values as 0. Handle exponential notation explicitly (or derive precision without string-dot parsing) so scientific-notation inputs are not silently rounded away. [logic error]

Severity Level: Major ⚠️
❌ Dashboard KPIs misrender extremely small non-zero metric values.
⚠️ Screen readers announce zero instead of tiny non-zero values.
Steps of Reproduction ✅
1. Load the dashboard page implemented in `frontend/src/pages/Dashboard.tsx` (function
`Dashboard` starting at line 14) so it fetches summary data via `useSpaceSummary()` and
constructs KPI values at lines 23–75.

2. Configure or mock the backend summary response so `space_weather_index` (used as
`kpis[4].value` at lines 59–60 in `Dashboard.tsx`) is a very small non-zero number such as
`1e-7` (scientific notation in JavaScript).

3. Observe that when `summary.isLoading` and `summary.isError` are false, the KPI grid
renders `<AnimatedCounter value={kpi.value} />` at
`frontend/src/pages/Dashboard.tsx:118–120` for finite numeric `kpi.value`, passing the
tiny `1e-7` value into `AnimatedCounter` at
`frontend/src/components/AnimatedCounter.tsx:27–34`.

4. Inside `AnimatedCounter`, `displayText` is computed by `formatNumber(displayValue,
formatOptions)` at `AnimatedCounter.tsx:72`, which calls `inferDecimals(value)` in
`frontend/src/utils/numberFormatter.ts:28–33`; for `1e-7`, `value.toString()` yields an
exponential-form string with no `.` so `inferDecimals` returns `0`, causing
`Intl.NumberFormat` at `numberFormatter.ts:53–58` to round the non-zero value to `"0"` and
the dashboard KPI to display `0` instead of the intended tiny number.

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/numberFormatter.ts
**Line:** 30:32
**Comment:**
	*Logic Error: `inferDecimals` assumes decimal numbers always contain a `.` in `toString()`, but JavaScript renders very small/large floats in exponential form (for example `1e-7`), so this path returns `0` decimals and formats non-zero values as `0`. Handle exponential notation explicitly (or derive precision without string-dot parsing) so scientific-notation inputs are not silently rounded away.

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 thread frontend/src/utils/numberFormatter.ts Outdated
Comment on lines +53 to +58
const formatted = new Intl.NumberFormat(locale, {
notation: compact ? 'compact' : 'standard',
minimumFractionDigits: compact ? 0 : decimals,
maximumFractionDigits: decimals,
useGrouping,
}).format(value);

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 creates a new Intl.NumberFormat instance on every call, and AnimatedCounter calls this repeatedly during rAF updates, causing avoidable allocation and GC churn in a hot render path. Cache/memoize formatters by (locale, compact, useGrouping, decimals) so frame-by-frame formatting stays lightweight. [performance]

Severity Level: Major ⚠️
⚠️ Extra allocations during KPI animations wasting CPU cycles.
⚠️ Potential frame drops on low-end devices or browsers.
Steps of Reproduction ✅
1. Open the dashboard implemented in `frontend/src/pages/Dashboard.tsx` and allow
`useSpaceSummary()` to resolve so the `kpis` array is built (lines 23–75) with numeric
`value` fields for tracked satellites, debris objects, alerts, collisions, space weather
index, and agent load.

2. When `summary.isLoading` and `summary.isError` are false, the KPI grid at
`Dashboard.tsx:96–131` renders `<AnimatedCounter value={kpi.value} />` at lines 118–120
for each KPI, causing six `AnimatedCounter` instances in
`frontend/src/components/AnimatedCounter.tsx` to mount.

3. Each `AnimatedCounter` calls `useAnimatedCounter(value, { duration, disableAnimation:
loading })` at `AnimatedCounter.tsx:35–38`; inside `useAnimatedCounter`
(`frontend/src/hooks/useAnimatedCounter.ts:47–139`), the `tick` function uses
`requestAnimationFrame` and `setAnimatedValue` (lines 105–114) to update `animatedValue`
on every frame for the full clamped duration (800–1500ms), triggering a React re-render
for each frame.

4. On every render while animating, `AnimatedCounter` computes `displayText =
formatNumber(displayValue, formatOptions)` at `AnimatedCounter.tsx:72`, which invokes
`formatNumber` in `frontend/src/utils/numberFormatter.ts:43–61`; this function constructs
a fresh `Intl.NumberFormat` instance on line 53 for each call and frame, so a single
dashboard load generates hundreds of formatter allocations across all counters, increasing
allocation and garbage-collection pressure in the hot animation path.

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/numberFormatter.ts
**Line:** 53:58
**Comment:**
	*Performance: This creates a new `Intl.NumberFormat` instance on every call, and `AnimatedCounter` calls this repeatedly during rAF updates, causing avoidable allocation and GC churn in a hot render path. Cache/memoize formatters by `(locale, compact, useGrouping, decimals)` so frame-by-frame formatting stays lightweight.

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 thread frontend/src/components/AnimatedCounter.tsx Outdated
Comment thread frontend/package.json Outdated
Comment thread frontend/src/components/AnimatedCounter.tsx

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/pages/Dashboard.tsx (1)

66-113: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle non-finite KPI values If a numeric KPI ever comes back non-finite, this falls through to the raw value instead of a placeholder.

🤖 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/pages/Dashboard.tsx` around lines 66 - 113, Update the kpis
definition to detect non-finite numeric values for every numeric KPI and use the
placeholder instead of exposing NaN or Infinity. Preserve valid numeric values
and the existing zero-value fallbacks for Debris Objects and Agent Runs.
🤖 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/AnimatedCounter.tsx`:
- Around line 43-51: Update the React.useEffect handling highlightOnIncrease in
AnimatedCounter so that when the increase condition is false, it explicitly
clears the highlight state while preserving the existing timer cleanup. Ensure
any non-increase update cancels the pending timer and leaves highlight set to
false, preventing it from remaining active indefinitely.
- Around line 89-104: Update the visually hidden live region in AnimatedCounter
so aria-live remains "polite" continuously, and defer updating its displayed
text until isAnimating becomes false. Use the existing finalText value and an
effect or equivalent settled-state mechanism to ensure each completed animation
produces a content mutation and announces only the final value.
- Around line 72-73: Update AnimatedCounter’s display formatting to derive the
decimal precision once from the settled target value, using the exported
inferDecimals helper from numberFormatter.ts, and pass that precision through
formatOptions when formatting displayValue. Leave finalText’s screen-reader
formatting unchanged.

In `@frontend/src/hooks/useAnimatedCounter.ts`:
- Around line 78-133: Update the animation bookkeeping in the useEffect flow and
tick callback to continuously track the actual displayed value in a ref whenever
setAnimatedValue is called. Use that ref for the unchanged-target check and as
the interpolation start value, while preserving first-render behavior and
syncing it when animations are skipped or completed so interrupted animations
continue from their current value and snap correctly when the target reverts.

In `@frontend/src/pages/Dashboard.tsx`:
- Around line 157-159: Update the KPI display expression in Dashboard’s KPI
rendering block so any non-finite or undefined kpi.value without a defined
fallback renders the established placeholder instead of falling through to the
raw value. Preserve fallback usage when provided and AnimatedCounter for finite
numeric values.

---

Outside diff comments:
In `@frontend/src/pages/Dashboard.tsx`:
- Around line 66-113: Update the kpis definition to detect non-finite numeric
values for every numeric KPI and use the placeholder instead of exposing NaN or
Infinity. Preserve valid numeric values and the existing zero-value fallbacks
for Debris Objects and Agent Runs.
🪄 Autofix (Beta)

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: 37283cde-3a5c-4743-ae66-01f24a36df05

📥 Commits

Reviewing files that changed from the base of the PR and between 68f705a and 20a3eb8.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • frontend/package.json
  • frontend/src/components/AnimatedCounter.tsx
  • frontend/src/components/StatisticCard.tsx
  • frontend/src/components/animated-counter.css
  • frontend/src/hooks/useAnimatedCounter.ts
  • frontend/src/main.tsx
  • frontend/src/pages/Dashboard.tsx
  • frontend/src/pages/LandingPage.tsx
  • frontend/src/styles/index.css
  • frontend/src/utils/numberFormatter.ts
💤 Files with no reviewable changes (1)
  • frontend/src/pages/LandingPage.tsx

Comment thread frontend/src/components/AnimatedCounter.tsx
Comment thread frontend/src/components/AnimatedCounter.tsx Outdated
Comment thread frontend/src/components/AnimatedCounter.tsx
Comment on lines +78 to +133
useEffect(() => {
if (shouldSkipAnimation) {
// Keep the refs in sync so a later switch back to animated mode
// starts from the right value instead of replaying from 0.
fromRef.current = target;
isFirstRun.current = false;
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
return;
}

// Nothing changed — spec explicitly says don't re-animate.
if (!isFirstRun.current && fromRef.current === target) {
return;
}

const from = isFirstRun.current ? 0 : fromRef.current;
const startTime = performance.now();

if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
}

let hasStarted = false;

const tick = (now: number) => {
if (!hasStarted) {
hasStarted = true;
setIsAnimating(true);
}
const elapsed = now - startTime;
const progress = Math.min(elapsed / clampedDuration, 1);
const eased = easeOutExpo(progress);
setAnimatedValue(from + (target - from) * eased);

if (progress < 1) {
rafRef.current = requestAnimationFrame(tick);
} else {
setAnimatedValue(target);
fromRef.current = target;
isFirstRun.current = false;
setIsAnimating(false);
rafRef.current = null;
}
};

rafRef.current = requestAnimationFrame(tick);

return () => {
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
}
};
}, [target, clampedDuration, shouldSkipAnimation]);

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

Interrupted animations can jump-restart or freeze mid-value.

fromRef/isFirstRun are only updated when an animation completes (L119-120) or is skipped (L82-83) — never when a new target interrupts one in flight. Two concrete breaks:

  • If the very first animation is interrupted before finishing, the next run still treats it as first-run (from = 0, L96), so the counter visibly jumps back to 0 and restarts instead of continuing from where it was.
  • If target reverts to exactly fromRef.current while mid-animation, the skip check (L92-94) short-circuits and the display stays frozen at whatever intermediate value it was interrupted at, never snapping to the new target.

Track the actual last-displayed value continuously (e.g. update a ref on every setAnimatedValue call) and use that as the interpolation start point / skip comparison, instead of only updating bookkeeping on natural completion.

♻️ Sketch of a fix
+  const currentValueRef = useRef(shouldSkipAnimation ? target : 0);
+
   useEffect(() => {
     if (shouldSkipAnimation) {
       fromRef.current = target;
+      currentValueRef.current = target;
       isFirstRun.current = false;
       ...
       return;
     }

-    if (!isFirstRun.current && fromRef.current === target) {
+    if (!isFirstRun.current && currentValueRef.current === target) {
       return;
     }

-    const from = isFirstRun.current ? 0 : fromRef.current;
+    const from = isFirstRun.current ? 0 : currentValueRef.current;
     ...
     const tick = (now) => {
       ...
-      setAnimatedValue(from + (target - from) * eased);
+      const next = from + (target - from) * eased;
+      currentValueRef.current = next;
+      setAnimatedValue(next);
       if (progress < 1) {
         ...
       } else {
         setAnimatedValue(target);
+        currentValueRef.current = target;
         fromRef.current = target;
         isFirstRun.current = false;
         ...
       }
     };
🤖 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/useAnimatedCounter.ts` around lines 78 - 133, Update the
animation bookkeeping in the useEffect flow and tick callback to continuously
track the actual displayed value in a ref whenever setAnimatedValue is called.
Use that ref for the unchanged-target check and as the interpolation start
value, while preserving first-render behavior and syncing it when animations are
skipped or completed so interrupted animations continue from their current value
and snap correctly when the target reverts.

Comment thread frontend/src/pages/Dashboard.tsx Outdated
@github-actions github-actions Bot added backend Backend development database Database and schema related changes size/XL Very large or complex contribution. type:backend Changes backend services or server-side logic. labels Jul 20, 2026
@codeant-ai codeant-ai Bot added size:XXL This PR changes 1000+ lines, ignoring generated files and removed size:L Large size labels Jul 20, 2026

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

♻️ Duplicate comments (1)
frontend/src/components/AnimatedCounter.tsx (1)

103-105: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not announce the new target before its animation settles.

After a settled counter receives a new value, isAnimating is still false for that render because the hook sets it only in the next RAF callback. Line 105 therefore updates this established polite region with finalText before the visual animation begins.

Expose a synchronously pending/settled value from useAnimatedCounter (or synchronously mark a changed target as animating), and update the live-region text only on animation completion.

Suggested hook-side direction
- const [isAnimating, setIsAnimating] = useState(false);
+ const [isAnimating, setIsAnimating] = useState(!shouldSkipAnimation);
+ const [animationTarget, setAnimationTarget] = useState(target);
+
+ if (!shouldSkipAnimation && animationTarget !== target) {
+   setAnimationTarget(target);
+   if (!isAnimating) setIsAnimating(true);
+ }
🤖 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/AnimatedCounter.tsx` around lines 103 - 105, Update
useAnimatedCounter so a changed value is synchronously treated as
pending/animating before the next render, preventing finalText from entering the
aria-live region immediately; expose the pending/settled state or synchronously
mark the target as animating, then update the AnimatedCounter live-region text
only once the animation completes.
🤖 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.

Duplicate comments:
In `@frontend/src/components/AnimatedCounter.tsx`:
- Around line 103-105: Update useAnimatedCounter so a changed value is
synchronously treated as pending/animating before the next render, preventing
finalText from entering the aria-live region immediately; expose the
pending/settled state or synchronously mark the target as animating, then update
the AnimatedCounter live-region text only once the animation completes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ad8bd218-b594-4d58-8d4d-b78b76e9c48a

📥 Commits

Reviewing files that changed from the base of the PR and between 20a3eb8 and 25516a8.

⛔ Files ignored due to path filters (48)
  • backend/__pycache__/debug_log.cpython-313.pyc is excluded by !**/*.pyc
  • backend/ai/__pycache__/__init__.cpython-313.pyc is excluded by !**/*.pyc
  • backend/ai/__pycache__/openai_service.cpython-313.pyc is excluded by !**/*.pyc
  • backend/ai/__pycache__/workflow.cpython-313.pyc is excluded by !**/*.pyc
  • backend/api/__pycache__/__init__.cpython-313.pyc is excluded by !**/*.pyc
  • backend/api/__pycache__/router.cpython-313.pyc is excluded by !**/*.pyc
  • 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__/error_handlers.cpython-313.pyc is excluded by !**/*.pyc
  • backend/app/core/__pycache__/exceptions.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/__pycache__/collision_engine.cpython-313.pyc is excluded by !**/*.pyc
  • backend/orbital/__pycache__/orbit_engine.cpython-313.pyc is excluded by !**/*.pyc
  • backend/orbital/__pycache__/spacetrack.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/package-lock.json is excluded by !**/package-lock.json
  • backend/schemas/__pycache__/__init__.cpython-313.pyc is excluded by !**/*.pyc
  • backend/schemas/__pycache__/api_schemas.cpython-313.pyc is excluded by !**/*.pyc
  • backend/services/__pycache__/__init__.cpython-313.pyc is excluded by !**/*.pyc
  • backend/services/__pycache__/risk_service.cpython-313.pyc is excluded by !**/*.pyc
  • backend/services/__pycache__/simulation_service.cpython-313.pyc is excluded by !**/*.pyc
  • backend/services/__pycache__/weather_service.cpython-313.pyc is excluded by !**/*.pyc
  • backend/websocket/__pycache__/__init__.cpython-313.pyc is excluded by !**/*.pyc
  • backend/websocket/__pycache__/manager.cpython-313.pyc is excluded by !**/*.pyc
  • frontend/package-lock.json is excluded by !**/package-lock.json
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • frontend/package.json
  • frontend/src/components/AnimatedCounter.tsx
  • frontend/src/hooks/__tests__/useAnimatedCounter.test.ts
  • frontend/src/hooks/useAnimatedCounter.ts
  • frontend/src/pages/Dashboard.tsx
  • frontend/src/utils/__tests__/numberFormatter.test.ts
  • frontend/src/utils/numberFormatter.ts
  • frontend/vitest.config.ts
  • package.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/hooks/useAnimatedCounter.ts
  • frontend/src/pages/Dashboard.tsx

@github-actions github-actions Bot added testing Tests added or improved type:testing Adds or updates automated tests. GitHub Actions GitHub Actions workflows and CI/CD github-actions GitHub Actions workflows and CI/CD labels Jul 20, 2026
@codeant-ai codeant-ai Bot added size:XXL This PR changes 1000+ lines, ignoring generated files and removed size:XXL This PR changes 1000+ lines, ignoring generated files labels Jul 22, 2026
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 GitHub Actions GitHub Actions workflows and CI/CD github-actions GitHub Actions workflows and CI/CD size/L Large contribution requiring significant work. size/XL Very large or complex contribution. size:XXL This PR changes 1000+ 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.

Add Animated Dashboard Counters for Real-Time Statistics

1 participant