Skip to content

feat: initial earth events dashboard with NASA EONET API#5

Merged
gamaware merged 4 commits into
mainfrom
feat/initial-dashboard
Apr 4, 2026
Merged

feat: initial earth events dashboard with NASA EONET API#5
gamaware merged 4 commits into
mainfrom
feat/initial-dashboard

Conversation

@gamaware

@gamaware gamaware commented Apr 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • Full-stack Next.js 16 dashboard visualizing real-time natural events from NASA's EONET API
  • Interactive Mapbox GL globe with dark/space aesthetic, clustering, and category-specific markers
  • 13 event categories with color-coded markers, filter chips, and instant client-side toggling
  • URL-synced filter state (category, status, date range, source) via nuqs for shareable views
  • API route caching proxies protecting EONET's 60 req/min rate limit
  • Responsive: desktop sidebar + mobile Vaul bottom drawer
  • Event detail panel with timeline, source links, and storm track visualization
  • Stats bar with animated counters (framer-motion)
  • Canonical repo scaffolding: pre-commit hooks, CI (lint/type-check/test/build), Semgrep+Trivy security scanning

Tech Stack

Layer Technology
Framework Next.js 16 (App Router)
Language TypeScript (strict)
Styling Tailwind CSS 4 + shadcn/ui
Map Mapbox GL JS (globe projection)
Data fetching SWR
Filter state nuqs (URL search params)
Testing Vitest (28 tests passing)

Test plan

  • Add Mapbox token to .env.local and run npm run dev
  • Verify globe renders with dark style and fog atmosphere
  • Verify events load and display as clustered markers
  • Toggle category filters and confirm instant response (no loading)
  • Change status/date filters and confirm data refresh
  • Click event marker -> verify detail panel opens and map flies to location
  • Test on mobile viewport (<768px) -> verify bottom drawer works
  • Run npm run build with zero errors
  • Run npx vitest run with 28/28 tests passing

Summary by CodeRabbit

  • New Features

    • Earth Events Dashboard: interactive map with live NASA EONET events, selectable markers, popups, storm tracks, detail panel with timeline, stats overlay, and mobile drawer.
    • Rich filtering and controls: category/status/time/source filters, shareable URL state, auto-refresh, and map controls.
  • Documentation

    • Added full README, contributing guide, security policy, contribution templates, ADRs, and usage instructions.
  • Chores

    • CI/quality workflows, pre-commit & lint/format rules, secret scanning, and dependency/update automation.
  • Tests

    • Added utility formatting tests.

Full-stack Next.js 16 dashboard for visualizing real-time natural events
using NASA's EONET API on an interactive Mapbox GL globe.

- 13 event categories with color-coded markers and clustering
- Filter by category, status, date range, and data source
- URL-synced filter state via nuqs for shareable views
- API route caching proxies (5min events, 24h categories)
- Dark NASA/space aesthetic with shadcn/ui components
- Responsive: desktop sidebar + mobile bottom drawer
- Event detail panel with timeline and source links
- Stats bar with animated counters
- Storm track visualization for multi-geometry events
- Canonical repo scaffolding (pre-commit, CI, security scanning)
Copilot AI review requested due to automatic review settings April 4, 2026 05:25
@coderabbitai

coderabbitai Bot commented Apr 4, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds repository tooling, CI/workflows, linters, pre-commit hooks, Claude post-edit automation; introduces an Earth Events Dashboard with Next.js API proxy routes to NASA EONET, TypeScript types, SWR hooks, MapLibre-based map integration, UI components, utilities, tests, docs, and styling.

Changes

Cohort / File(s) Summary
Repo configs & linting
/.gitignore, /.prettierrc, /.prettierignore, /.markdownlint.yaml, /.markdown-link-check.json, /.yamllint.yml, /.secrets.baseline, zizmor.yml, package.json, vitest.config.ts
Added/updated formatting, lint/link-check, YAML lint, secrets baseline, zizmor policy, swapped mapbox-glmaplibre-gl, and Vitest config with path alias.
Pre-commit & Claude hooks
/.pre-commit-config.yaml, /.claude/hooks/post-edit.sh, /.claude/settings.json
Configured pre-commit hooks and added a Claude PostToolUse hook plus post-edit script that optionally runs formatters and marks shell files executable.
GitHub templates & automation
.github/ISSUE_TEMPLATE/*, .github/PULL_REQUEST_TEMPLATE.md, .github/copilot-instructions.md, .coderabbit.yaml, .github/dependabot.yml, CODEOWNERS
Added issue/PR templates, Copilot instructions, auto-review config, Dependabot settings, and CODEOWNERS.
Workflows, actions & scripts
.github/workflows/..., .github/actions/..., .github/scripts/*
New CI, quality, security, and scheduled workflows; composite actions for security scan and pre-commit updates; repository validation and README check scripts.
Docs & policies
README.md, CONTRIBUTING.md, CLAUDE.md, SECURITY.md, LICENSE, AGENTS.md, docs/adr/*
Added/updated project docs, contribution guide, security policy, MIT license, CLAUDE instructions, and multiple ADRs.
Global styles & layout
src/app/globals.css, src/app/layout.tsx, src/app/page.tsx
Reworked theme tokens/colors, added category color vars, MapLibre popup styles, animation class, metadata changes, NuqsAdapter wrapper, and page now renders DashboardShell in Suspense.
Server API proxies
src/app/api/eonet/.../route.ts (categories, events, geojson, sources)
New Next.js API routes proxying NASA EONET endpoints with query forwarding, revalidation, cache headers, and 502 error handling.
API client & types
src/lib/api/eonet-client.ts, src/lib/api/types.ts
EONET client functions and TypeScript domain/GeoJSON types and filter state interfaces.
Data hooks & filters
src/lib/hooks/use-events.ts, use-geojson.ts, use-categories.ts, use-filters.ts
SWR hooks for events/geojson/categories and URL-synced filter state using nuqs.
Constants & utils
src/lib/constants/*, src/lib/utils/format.ts
Map and category configs plus formatting utilities (date/time, magnitude, coords, timeAgo).
Map integration
src/components/map/... (map-provider, map-container, event-markers, event-popup, map-controls, storm-tracks)
MapLibre provider/container, clustered GeoJSON source and layers, hover/selected popups, controls, and storm-track polylines with category filtering.
Dashboard shell & layout
src/components/layout/* (dashboard-shell, header, sidebar, mobile-drawer)
Dashboard orchestration: filter state, data fetching, auto-refresh toggle, responsive sidebar/drawer, header with command/search and refresh UI.
Events UI
src/components/events/* (event-card, event-list, event-detail-panel, event-timeline)
Event cards, list with skeleton/empty state, detail sheet with timeline and sources, and geometry timeline.
Filters components
src/components/filters/* (category-filter, status-filter, date-range-filter, source-filter)
Category/status/date-range/source filter components with accessible controls and handlers.
Stats & UI primitives
src/components/stats/*, src/components/ui/*
Stats bar and animated stat cards; many UI primitives (badge, card, input, input-group, textarea, scroll-area, separator, skeleton, toggle, toggle-group, dialog, sheet, command palette, etc.).
Tests
src/__tests__/lib/format.test.ts
Vitest tests for formatting utilities including timeAgo behavior and edge cases.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Browser UI
  participant Next as Next.js Server (API)
  participant EONET as NASA EONET API
  participant MapComp as MapLibre Component

  Client->>Next: GET /api/eonet/events?{filters}
  Next->>EONET: fetch /api/v3/events?{filters}
  EONET-->>Next: JSON events
  Next-->>Client: proxied JSON (cache headers)
  Client->>MapComp: supply GeoJSON/events -> render clusters/markers
  Client->>MapComp: click marker -> client selects event -> update UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped through hooks and linted nights,
I polished colors, fixed the plights,
Markers shimmer, popups sing,
Data dances, dashboards spring.
Huzzah—code tidy, tests alight!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/initial-dashboard

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

Adds an initial full-stack “Earth Events” dashboard to the Next.js App Router app, integrating NASA EONET v3 via cached API routes and rendering events on an interactive Mapbox GL globe with URL-synced filters.

Changes:

  • Introduces EONET proxy API routes (events/geojson/categories/sources) plus SWR hooks and shared API/types/constants for client consumption.
  • Implements the dashboard UI (map, markers/clustering, storm tracks, popups, detail panel, filters, responsive sidebar/drawer, stats bar).
  • Adds repo scaffolding for quality/security (Vitest config + tests, ESLint rules, pre-commit, CI workflows, security docs, ADRs).

Reviewed changes

Copilot reviewed 86 out of 87 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
zizmor.yml Configures zizmor policy for GitHub Actions security checks.
vitest.config.ts Adds Vitest configuration (jsdom, coverage, alias).
src/lib/utils/format.ts Adds date/time/magnitude/coordinate formatting helpers.
src/lib/hooks/use-geojson.ts SWR hook for GeoJSON endpoint with querystring builder.
src/lib/hooks/use-filters.ts URL-synced filter state via nuqs parsers.
src/lib/hooks/use-events.ts SWR hook for EONET events endpoint with querystring builder.
src/lib/hooks/use-categories.ts SWR hook for categories endpoint with aggressive no-revalidate.
src/lib/constants/map-config.ts Centralizes Mapbox style/projection/fog/cluster defaults.
src/lib/constants/categories.ts Defines category palette/icons and lookup helpers.
src/lib/api/types.ts Adds TypeScript interfaces for EONET responses + filters.
src/lib/api/eonet-client.ts Adds direct EONET client functions (events/geojson/categories/sources).
src/components/ui/toggle.tsx Adds Base UI Toggle wrapper with styling variants.
src/components/ui/toggle-group.tsx Adds Base UI ToggleGroup wrappers for filter chips.
src/components/ui/textarea.tsx Adds styled textarea component.
src/components/ui/skeleton.tsx Adds skeleton loading component.
src/components/ui/sheet.tsx Adds sheet (drawer/panel) primitives used by detail panel.
src/components/ui/separator.tsx Adds separator primitive.
src/components/ui/scroll-area.tsx Adds scroll area primitive.
src/components/ui/input.tsx Adds styled input primitive.
src/components/ui/input-group.tsx Adds composed input group primitives used by command palette.
src/components/ui/dialog.tsx Adds dialog primitives used by command palette.
src/components/ui/command.tsx Adds cmdk-based command palette components.
src/components/ui/card.tsx Adds card primitives used across UI.
src/components/ui/badge.tsx Adds badge component with variants.
src/components/stats/stats-bar.tsx Adds stats bar showing totals and top categories.
src/components/stats/stat-card.tsx Adds stat card with animated number + loading skeleton.
src/components/map/storm-tracks.tsx Adds storm track LineString overlay rendering.
src/components/map/map-provider.tsx Provides Mapbox map instance via React context.
src/components/map/map-controls.tsx Adds zoom/reset controls for the map.
src/components/map/map-container.tsx Initializes Mapbox GL map and handles resize lifecycle.
src/components/map/event-popup.tsx Adds hover/selected popups for map features.
src/components/map/event-markers.tsx Adds GeoJSON source, clustering, layers, filtering, and click handlers.
src/components/layout/sidebar.tsx Desktop sidebar with filters + event list.
src/components/layout/mobile-drawer.tsx Mobile bottom drawer with filters + event list.
src/components/layout/header.tsx Header with search (command palette) and refresh toggle.
src/components/layout/dashboard-shell.tsx Composes layout, URL filters, SWR data, map, and panels.
src/components/filters/status-filter.tsx Toggle-group status filter.
src/components/filters/source-filter.tsx Source multi-select filter UI.
src/components/filters/date-range-filter.tsx Toggle-group date range filter.
src/components/filters/category-filter.tsx Category chip filter UI with color coding.
src/components/events/event-timeline.tsx Timeline UI for event geometry history.
src/components/events/event-list.tsx Event list rendering + empty/loading states.
src/components/events/event-detail-panel.tsx Sliding detail panel with sources/timeline/magnitude.
src/components/events/event-card.tsx Event list item card with category/date/magnitude.
src/app/page.tsx Replaces starter page with Suspense-wrapped dashboard shell.
src/app/layout.tsx Adds NuqsAdapter provider + dark-only base layout + metadata updates.
src/app/globals.css Adds dark theme tokens + Mapbox popup overrides.
src/app/api/eonet/sources/route.ts Cached proxy route for EONET sources.
src/app/api/eonet/geojson/route.ts Cached proxy route for EONET GeoJSON with passthrough params.
src/app/api/eonet/events/route.ts Cached proxy route for EONET events with passthrough params.
src/app/api/eonet/categories/route.ts Cached proxy route for EONET categories.
src/tests/lib/format.test.ts Adds unit tests for formatting utilities.
SECURITY.md Adds vulnerability reporting/security practices doc.
README.md Replaces starter README with product/setup/architecture docs.
LICENSE Adds MIT license file.
eslint.config.mjs Tightens ESLint rules (no-eval, no any, etc.).
docs/adr/003-url-params-for-filter-state.md ADR documenting nuqs URL state choice.
docs/adr/002-raw-mapbox-over-react-map-gl.md ADR documenting using raw mapbox-gl.
docs/adr/001-nextjs-over-spa.md ADR documenting Next.js choice.
CONTRIBUTING.md Adds contributor workflow and standards.
CODEOWNERS Adds repo-wide codeowner.
CLAUDE.md Adds repository conventions/instructions doc.
AGENTS.md Removes prior agent guidance stub.
.yamllint.yml Adds yamllint configuration.
.secrets.baseline Adds detect-secrets baseline file.
.prettierrc Adds Prettier configuration.
.prettierignore Adds Prettier ignore rules.
.pre-commit-config.yaml Adds pre-commit hooks for formatting/lint/security.
.markdownlint.yaml Adds markdownlint configuration.
.markdown-link-check.json Adds markdown link checker configuration.
.gitignore Expands ignore rules for Next.js/IDE/env/certs.
.github/workflows/update-pre-commit-hooks.yml Scheduled workflow to autoupdate pre-commit hooks.
.github/workflows/security.yml Adds security scanning workflow.
.github/workflows/quality-checks.yml Adds markdown/yaml/link/structure/actions security/shell checks.
.github/workflows/ci.yml Adds CI for lint/typecheck/test/build.
.github/scripts/validate-structure.sh Validates required repo files/directories.
.github/scripts/check-readme.sh Enforces README quality checks.
.github/PULL_REQUEST_TEMPLATE.md Adds PR template with checklist.
.github/ISSUE_TEMPLATE/feature-request.md Adds feature request issue template.
.github/ISSUE_TEMPLATE/bug-report.md Adds bug report issue template.
.github/dependabot.yml Enables weekly Dependabot updates.
.github/copilot-instructions.md Adds Copilot review priorities/conventions doc.
.github/actions/update-pre-commit-composite/action.yml Composite action to autoupdate hooks and open PR.
.github/actions/security-scan/action.yml Composite action to run Semgrep + Trivy and upload SARIF.
.coderabbit.yaml Configures CodeRabbit review scopes/instructions.
.claude/settings.json Configures Claude Code hooks.
.claude/hooks/post-edit.sh Post-edit formatting hook script.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/map/event-popup.tsx Outdated
Comment on lines +104 to +112
hoverPopupRef.current = new mapboxgl.Popup({
closeButton: false,
closeOnClick: false,
offset: 12,
className: "event-hover-popup",
})
.setLngLat(coords)
.setHTML(buildPopupHTML(parseProperties(properties)))
.addTo(map);

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

setHTML(buildPopupHTML(...)) injects unescaped values from feature.properties (e.g. title, date, magnitudeUnit) into raw HTML, which creates an XSS risk because this data originates from an external API. Prefer building the popup via DOM nodes (e.g. setDOMContent) or escaping/sanitizing interpolated strings before passing them to setHTML.

Copilot uses AI. Check for mistakes.
Comment on lines +122 to +124
map.on("mousemove", LAYER_UNCLUSTERED, handleMouseMove);
map.on("mouseleave", LAYER_UNCLUSTERED, handleMouseLeave);

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

This effect registers map.on('mousemove', LAYER_UNCLUSTERED, ...) / map.on('mouseleave', LAYER_UNCLUSTERED, ...) as soon as isLoaded is true, but it doesn't verify that the unclustered-point layer exists yet. Mapbox GL will throw if you attach layer-specific handlers before the layer is added, so add a map.getLayer(LAYER_UNCLUSTERED) guard (or delay binding until markers are added).

Copilot uses AI. Check for mistakes.
Comment on lines +150 to +153
const features = map.querySourceFeatures("events", {
filter: ["==", ["get", "id"], selectedEventId],
});

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

map.querySourceFeatures('events', ...) assumes the events source is already registered, but if a user selects an event before EventMarkers has added the source (or before GeoJSON data arrives), Mapbox will throw for an unknown source id. Add a guard like if (!map.getSource('events')) return; (or coordinate selection popup rendering with the marker/source lifecycle).

Copilot uses AI. Check for mistakes.
Comment on lines +198 to +207
useEffect(() => {
if (!map || !isLoaded) {
return;
}

const handleClusterClick = (e: MapMouseEvent) => {
const features = map.queryRenderedFeatures(e.point, {
layers: [LAYER_CLUSTERS],
});
if (!features.length) {

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

This effect binds click/hover handlers for clusters / unclustered-point whenever map && isLoaded, but it doesn't wait for addSourceAndLayers() to run (i.e. layers may not exist yet if GeoJSON data hasn't loaded). Attaching layer-scoped handlers before the layer exists can throw; consider gating this effect on layersAdded.current or checking map.getLayer(...) before calling map.on(...).

Copilot uses AI. Check for mistakes.
Comment thread src/components/map/event-markers.tsx Outdated
Comment on lines +129 to +141
"circle-color": buildCategoryColorExpression(),
"circle-radius": [
"interpolate",
["linear"],
["remainder", ["/", ["to-number", ["get", "date"], 0], 1000], 4],
0,
8,
2,
14,
4,
8,
],
"circle-opacity": 0.3,

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

The pulse layer's circle-radius expression is based on ['to-number', ['get','date'], 0], but properties.date is an ISO string in the EONET GeoJSON, so to-number will fall back to 0 and the pulse will effectively be static. Consider enriching features with a numeric timestamp (e.g. dateMs) and using that in the expression (or drive pulsing via feature-state + setFeatureState).

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +33
for (const event of events) {
const points = event.geometry.filter((g) => g.type === "Point");
if (points.length < 2) {
continue;
}

const coordinates = points.map((g) => g.coordinates as [number, number]);
const categoryId =

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

Storm track coordinates are built from event.geometry.filter((g) => g.type === 'Point') but the points aren't sorted by g.date. EONET geometry arrays are not guaranteed to be chronological, so the resulting LineString can zig-zag. Sort the points by date before mapping to coordinates.

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +58
return (
<ScrollArea className="h-full">
<div className="flex flex-col gap-1.5">

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

Sidebar wraps EventList in a ScrollArea, but EventList also renders its own ScrollArea, creating nested scroll containers which can cause awkward scrolling and broken focus/keyboard behavior. Consider letting only the layout own scrolling (remove the inner ScrollArea in EventList, or remove the outer one).

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +51
const events = useMemo(() => eventsData?.events ?? [], [eventsData]);
const geojsonFeatureCollection =
(geojsonData as unknown as GeoJSON.FeatureCollection) ?? null;

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

geojsonData is being coerced via a double assertion to GeoJSON.FeatureCollection, which bypasses type-checking and can hide incompatibilities between the EONET response shape and what Mapbox expects. Prefer typing useGeoJSON() to return a GeoJSON.FeatureCollection-compatible type (or make EONETGeoJSONResponse extend GeoJSON.FeatureCollection) so this cast isn't needed.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +17
interface MapContextValue {
map: mapboxgl.Map | null;
setMap: (map: mapboxgl.Map) => void;
isLoaded: boolean;
setIsLoaded: (loaded: boolean) => void;
}

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

MapContextValue.setMap only accepts a non-null map instance, so MapContainer can't clear the context map on unmount and consumers may retain a reference to a removed map. Consider changing setMap to accept mapboxgl.Map | null and calling setMap(null) during cleanup to avoid stale references.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +16
## Decision

Use Next.js 15 with App Router.

## Rationale

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

This ADR states "Use Next.js 15 with App Router", but package.json pins Next.js 16.2.2 and the PR description/README also reference Next.js 16. Update the ADR to match the actual framework version to avoid future confusion.

Copilot uses AI. Check for mistakes.

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

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (21)
src/components/ui/skeleton.tsx-5-9 (1)

5-9: ⚠️ Potential issue | 🟡 Minor

Hide decorative skeletons from screen readers by default.

Line 5 renders a visual-only placeholder; add aria-hidden="true" so it isn’t announced by assistive tech.

Proposed fix
 function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
   return (
     <div
       data-slot="skeleton"
       className={cn("animate-pulse rounded-md bg-muted", className)}
       {...props}
+      aria-hidden="true"
     />
   )
 }
As per coding guidelines, `src/components/**` and `**/*.tsx` must be reviewed for accessibility (ARIA labels/semantic accessibility checks).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/skeleton.tsx` around lines 5 - 9, Add aria-hidden="true" to
the visual-only placeholder div (the element with data-slot="skeleton" and
className={cn("animate-pulse rounded-md bg-muted", className)}) so screen
readers ignore it; ensure this default is applied even when props are spread by
either placing the aria-hidden attribute after {...props} or merging props to
set aria-hidden to true when not explicitly provided.
src/components/ui/toggle-group.tsx-43-43 (1)

43-43: ⚠️ Potential issue | 🟡 Minor

Avoid inline style for spacing variable in component code.

style={{ "--gap": spacing }} violates the no-inline-style component rule.

Prefer variant/class-based spacing tokens (e.g., data-spacing + class variants) instead of runtime inline style.

As per coding guidelines, src/components/**: “Review for React best practices: proper prop typing, accessibility (aria labels, keyboard navigation), memoization where appropriate, and no inline styles.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/toggle-group.tsx` at line 43, The inline style using the
CSS variable (style={{ "--gap": spacing }}) in the ToggleGroup component should
be removed; instead, map the spacing prop to a data attribute (e.g.,
data-spacing={spacing}) on the root element of the ToggleGroup and create
corresponding CSS/class variants that set --gap for each allowed spacing token.
Update the ToggleGroup component (toggle-group.tsx) to stop using inline style,
add/emit a typed spacing prop if not present, and add CSS rules or className
variants (e.g., [data-spacing="sm"], [data-spacing="md"], [data-spacing="lg"])
that assign --gap values so layout uses the CSS variable without runtime inline
styles.
src/components/stats/stat-card.tsx-62-70 (1)

62-70: ⚠️ Potential issue | 🟡 Minor

Replace inline color styles with class/token-based styling.

Inline style is used for icon and value color; this breaks the component styling rule.

A practical fix is to pass a semantic color class (e.g., text-category-wildfires) instead of raw hex and apply it via className.

As per coding guidelines, src/components/**: “Review for React best practices: proper prop typing, accessibility (aria labels, keyboard navigation), memoization where appropriate, and no inline styles.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/stats/stat-card.tsx` around lines 62 - 70, The component is
using inline style for color on the icon span and the value span; remove those
inline style usages and switch to class/token-based styling by accepting and
applying a semantic color class (e.g., colorClass or textColorClass) instead of
raw hex in the StatCard/StatCardProps (or the component that renders the icon
and value); update references to the prop in the icon span and the value span to
add the className (e.g., `${colorClass} shrink-0` and `${colorClass} text-sm
font-bold ...`) and ensure the prop is typed in StatCardProps and documented so
callers pass a semantic token like "text-category-wildfires" rather than inline
styles.
src/components/map/map-container.tsx-57-62 (1)

57-62: ⚠️ Potential issue | 🟡 Minor

Clear map context during cleanup to avoid stale map references.

Unmount cleanup resets isLoaded but leaves the provider’s map value untouched. Consumers can still hold/use a removed instance.

Suggested fix
     return () => {
       resizeObserver.disconnect();
       setIsLoaded(false);
+      setMap(null);
       map.remove();
       mapRef.current = null;
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/map/map-container.tsx` around lines 57 - 62, Cleanup currently
disconnects the ResizeObserver and removes the Leaflet map but leaves the
provider’s `map` value pointing at the removed instance; update the unmount
cleanup in map-container (where `resizeObserver`, `setIsLoaded`, `map.remove()`,
and `mapRef.current` are handled) to also clear the provider map reference by
calling the context updater (e.g. `setMap(null)` or the provider's equivalent)
so consumers cannot access a stale/removed map instance.
src/components/filters/category-filter.tsx-22-24 (1)

22-24: ⚠️ Potential issue | 🟡 Minor

Set type="button" on filter buttons.

Without an explicit type, these controls can submit surrounding forms unexpectedly.

💡 Proposed fix
           <button
             key={id}
+            type="button"
             onClick={() => onToggle(id)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/filters/category-filter.tsx` around lines 22 - 24, The filter
buttons in the CategoryFilter component are missing an explicit type which can
cause them to submit surrounding forms; update the JSX for the button rendered
with key={id} and onClick={() => onToggle(id)} to include type="button" so it
won't act as a submit control, keeping the existing onToggle handler and id
usage unchanged.
src/components/filters/date-range-filter.tsx-33-37 (1)

33-37: ⚠️ Potential issue | 🟡 Minor

Fix the “All” option ARIA label wording.

For the All option, the current label resolves to “Show events from last All”, which is not screen-reader friendly.

💡 Proposed fix
-          aria-label={`Show events from last ${option.label}`}
+          aria-label={
+            option.value === 0
+              ? "Show events from all time"
+              : `Show events from last ${option.label}`
+          }

As per coding guidelines, **/*.tsx: "Review for accessibility (alt text, semantic HTML, ARIA labels), proper TypeScript typing, and no use of any type."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/filters/date-range-filter.tsx` around lines 33 - 37, The ARIA
label on ToggleGroupItem currently produces "Show events from last All" for the
All option; update the aria-label logic in the rendering of ToggleGroupItem (the
component using option.value and option.label) to conditionally use a different
string when option.value or option.label indicates the all option (e.g., if
option.value === 'all' or option.label.toLowerCase() === 'all' then set
aria-label to "Show all events" or "Show events from all time"), otherwise keep
the existing "Show events from last {option.label}" wording so screen readers
get an appropriate label.
src/lib/utils/format.ts-35-41 (1)

35-41: ⚠️ Potential issue | 🟡 Minor

Guard formatCoordinates against non-finite values.

NaN/Infinity currently pass through and can produce malformed output like NaN°N.

💡 Proposed fix
 export function formatCoordinates(coords: number[]): string {
   if (!coords || coords.length < 2) return "N/A";

   const [lng, lat] = coords;
+  if (!Number.isFinite(lng) || !Number.isFinite(lat)) return "N/A";
   const latDir = lat >= 0 ? "N" : "S";
   const lngDir = lng >= 0 ? "E" : "W";

As per coding guidelines, src/lib/**: "Review for proper TypeScript typing, error handling, and separation of concerns. API clients should handle errors gracefully."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/utils/format.ts` around lines 35 - 41, The formatCoordinates function
currently formats coords without validating numeric finiteness; update the guard
in formatCoordinates to also check that coords[0] and coords[1] are finite
numbers (e.g., use Number.isFinite on lng and lat) and return "N/A" if either is
not finite, then proceed to compute lat, lng, latDir, lngDir and the formatted
string only when both values are finite; reference the coords array and the
lat/lng variables in your fix.
src/components/ui/dialog.tsx-111-115 (1)

111-115: ⚠️ Potential issue | 🟡 Minor

Missing accessible label on DialogFooter close button.

The close button in DialogContent (lines 73-75) correctly includes <span className="sr-only">Close</span> for screen reader users, but the close button in DialogFooter lacks this accessible label. This inconsistency means the footer close button won't have an accessible name.

♿ Proposed fix to add accessible label
       {showCloseButton && (
-        <DialogPrimitive.Close render={<Button variant="outline" />}>
-          Close
-        </DialogPrimitive.Close>
+        <DialogPrimitive.Close render={<Button variant="outline" />} aria-label="Close">
+          Close
+        </DialogPrimitive.Close>
       )}

Alternatively, since the text "Close" is visible, it may already serve as the accessible name. However, adding aria-label ensures consistency with the icon-only close button behavior.

As per coding guidelines: src/components/**: "Review for React best practices: proper prop typing, accessibility (aria labels, keyboard navigation)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/dialog.tsx` around lines 111 - 115, The DialogFooter close
button (DialogPrimitive.Close used with <Button variant="outline" />) is missing
an accessible label; update the DialogFooter close button to provide an
accessible name—either add a visually-hidden span (e.g., <span
className="sr-only">Close</span>) inside the Close button or add an explicit
aria-label="Close" on the DialogPrimitive.Close/Button so it matches the
DialogContent close button's accessibility; ensure you modify the DialogFooter
block that renders DialogPrimitive.Close and Button to include the chosen
accessible label.
CLAUDE.md-28-29 (1)

28-29: ⚠️ Potential issue | 🟡 Minor

Update action naming in structure docs to match the repo.

Line 28 says update-pre-commit, but the action in this PR is update-pre-commit-composite. Please align naming to keep the structure section accurate.

Suggested doc fix
-  actions/              # Composite actions (security-scan, update-pre-commit)
+  actions/              # Composite actions (security-scan, update-pre-commit-composite)

As per coding guidelines, CLAUDE.md: Verify that the documented structure, hooks, and CI/CD sections match the actual repository state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` around lines 28 - 29, Update the actions listing in CLAUDE.md so
the action name matches the repository: replace the documented
"update-pre-commit" entry under the actions/ section with
"update-pre-commit-composite" (i.e., ensure the actions/ comment line and any
other mentions of update-pre-commit are renamed to update-pre-commit-composite
to reflect the actual composite action introduced in this PR).
docs/adr/001-nextjs-over-spa.md-14-14 (1)

14-14: ⚠️ Potential issue | 🟡 Minor

Update ADR to reflect Next.js 16 instead of Next.js 15.

Line 14 documents "Next.js 15 with App Router" but the actual implementation uses Next.js 16.2.2 (per package.json). Update the ADR to match the current version.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/adr/001-nextjs-over-spa.md` at line 14, The ADR text currently states
"Next.js 15 with App Router" but the project uses Next.js 16.2.2; update the ADR
by replacing the phrase "Next.js 15 with App Router" with "Next.js 16 (16.2.2)
with App Router" (or similar wording reflecting v16), and scan the document for
any other occurrences of "Next.js 15" to make them consistent with the
package.json "next": "16.2.2" entry.
src/components/stats/stats-bar.tsx-36-42 (1)

36-42: ⚠️ Potential issue | 🟡 Minor

Make the total label follow the selected status.

This count is events.length, but the dashboard supports open | closed | all. When the user switches away from open, the card still says "Active Events" even though the dataset can be closed or mixed. A neutral "Events" label or a status prop would keep the copy accurate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/stats/stats-bar.tsx` around lines 36 - 42, The "Active Events"
label is static but should reflect the current selected status (open | closed |
all); update the code that renders the StatCard (component: StatCard, props:
label, value currently set from events.length) to derive both the count and the
label from the selected status (e.g., selectedStatus or filterStatus): compute
value as the filtered count when status !== 'all' and set label to "Active
Events" for 'open', "Closed Events" for 'closed', and a neutral "Events" for
'all' (or alternatively pass the status into StatCard via a new status prop and
let StatCard pick the label). Ensure you reference the existing events array and
the selected status variable when computing the display count and label.
src/lib/hooks/use-events.ts-13-18 (1)

13-18: ⚠️ Potential issue | 🟡 Minor

Preserve the API route's error payload.

Every failure is flattened to the status code, so callers lose the proxy's actual message and upstream reason. Read the response body before throwing and fall back to the status only if parsing fails.

🛠️ Suggested change
 async function fetcher(url: string): Promise<EONETEventsResponse> {
   const response = await fetch(url);
   if (!response.ok) {
-    throw new Error(`Events fetch failed: ${response.status}`);
+    const errorBody = (await response.json().catch(() => null)) as
+      | { error?: string; message?: string }
+      | null;
+    throw new Error(
+      errorBody?.error ??
+        errorBody?.message ??
+        `Events fetch failed: ${response.status}`,
+    );
   }
   return response.json() as Promise<EONETEventsResponse>;
 }
As per coding guidelines, `src/lib/**`: "Review for proper TypeScript typing, error handling, and separation of concerns. API clients should handle errors gracefully."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/hooks/use-events.ts` around lines 13 - 18, The fetcher function
currently throws only the HTTP status, losing upstream error details; modify
fetcher to read the response body when !response.ok, attempt to parse JSON (or
fallback to text) to extract the API's error payload, and include that payload
(or the parsed message) in the thrown Error (fall back to `response.status` if
parsing yields nothing); keep the return type for successful responses as
Promise<EONETEventsResponse> and ensure the thrown Error message includes both
the status and the preserved payload for debugging.
src/lib/hooks/use-geojson.ts-13-18 (1)

13-18: ⚠️ Potential issue | 🟡 Minor

Preserve the API route's error payload.

Every failure is flattened to the status code, so callers lose the proxy's actual message and upstream reason. Read the response body before throwing and fall back to the status only if parsing fails.

🛠️ Suggested change
 async function fetcher(url: string): Promise<EONETGeoJSONResponse> {
   const response = await fetch(url);
   if (!response.ok) {
-    throw new Error(`GeoJSON fetch failed: ${response.status}`);
+    const errorBody = (await response.json().catch(() => null)) as
+      | { error?: string; message?: string }
+      | null;
+    throw new Error(
+      errorBody?.error ??
+        errorBody?.message ??
+        `GeoJSON fetch failed: ${response.status}`,
+    );
   }
   return response.json() as Promise<EONETGeoJSONResponse>;
 }
As per coding guidelines, `src/lib/**`: "Review for proper TypeScript typing, error handling, and separation of concerns. API clients should handle errors gracefully."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/hooks/use-geojson.ts` around lines 13 - 18, The fetcher function
flattens errors to just the status code, losing the proxy/API error payload;
update fetcher(url: string): Promise<EONETGeoJSONResponse> to read the response
body when !response.ok (attempt to parse JSON then text), extract an informative
message (e.g., body.message or raw text), and throw a new Error that includes
the upstream payload plus the status as a fallback if parsing fails so callers
of fetcher can see the proxy's actual error details.
.claude/hooks/post-edit.sh-21-23 (1)

21-23: ⚠️ Potential issue | 🟡 Minor

Use npx --no-install prettier to respect repo dependency scope.

The repo has .prettierrc but prettier is not in package.json dependencies. Running plain npx prettier can resolve to a globally installed version or trigger installation outside the repo's pinned setup. Use npx --no-install prettier to ensure the formatter only runs if explicitly available in the project, failing gracefully otherwise.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/hooks/post-edit.sh around lines 21 - 23, Update the post-edit Git
hook's npx invocation so it won't pull or use a global/preinstalled prettier; in
the case branch that currently calls npx prettier within
.claude/hooks/post-edit.sh (the block guarded by "if command -v npx > /dev/null
2>&1; then" and the pattern matching "*.ts | *.tsx | *.js | *.jsx | *.css)"),
replace the plain "npx prettier" call with "npx --no-install prettier" so the
hook only runs the repo-local/preinstalled prettier and otherwise fails
gracefully; preserve the existing redirection and "|| true" behavior so the hook
remains tolerant of missing formatter.
src/components/filters/source-filter.tsx-54-66 (1)

54-66: ⚠️ Potential issue | 🟡 Minor

Active-source badges lack keyboard accessibility.

The Badge elements are interactive (clickable to remove sources) but may not be keyboard-navigable. Screen reader and keyboard-only users cannot tab to these badges or activate them with Enter/Space.

Consider using a <button> element instead, or add role="button", tabIndex={0}, and onKeyDown handlers.

♿ Proposed fix to improve keyboard accessibility
-            <Badge
-              key={source}
-              variant="secondary"
-              className="cursor-pointer text-xs"
-              onClick={() => handleToggle(source)}
-            >
-              {source}
-              <span className="ml-1" aria-hidden="true">
-                &times;
-              </span>
-            </Badge>
+            <button
+              key={source}
+              type="button"
+              onClick={() => handleToggle(source)}
+              className="inline-flex items-center rounded-md bg-secondary px-2 py-0.5 text-xs text-secondary-foreground hover:bg-secondary/80 cursor-pointer"
+              aria-label={`Remove source filter: ${source}`}
+            >
+              {source}
+              <span className="ml-1" aria-hidden="true">
+                &times;
+              </span>
+            </button>

As per coding guidelines: src/components/**: Review for accessibility (aria labels, keyboard navigation).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/filters/source-filter.tsx` around lines 54 - 66, The active
source badges rendered in the activeSources.map are interactive but not
keyboard-accessible; update the Badge usage (or replace it with a semantic
<button>) so each item that calls handleToggle(source) is focusable and
activatable via keyboard: either render a button element styled like the Badge
or add role="button", tabIndex={0}, aria-label (e.g., `Remove ${source}`) and an
onKeyDown handler that calls handleToggle(source) on Enter/Space; ensure the key
prop, variant, className and displayed text remain the same and that
handleToggle is used for both click and keyboard activation.
src/components/layout/sidebar.tsx-12-37 (1)

12-37: ⚠️ Potential issue | 🟡 Minor

Remove unused categories prop from interface and component.

The categories prop is declared in SidebarProps (line 14) but is never destructured or used anywhere in the component. The parent component still passes it, so removing it requires updating both the interface definition and the call sites in dashboard-shell.tsx.

Changes needed
  1. In src/components/layout/sidebar.tsx: Remove categories: string[] from the SidebarProps interface
  2. In src/components/layout/dashboard-shell.tsx: Remove categories={filters.categories} from both the <Sidebar> component (line 114) and <MobileDrawer> component (line 144)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/sidebar.tsx` around lines 12 - 37, Remove the unused
categories prop: delete "categories: string[]" from the SidebarProps interface
and remove it from the Sidebar component props if present; then update call
sites by removing categories={filters.categories} passed into <Sidebar> and
<MobileDrawer> in the dashboard-shell (remove those prop occurrences so the
component invocation matches the new SidebarProps). Ensure you only remove the
prop declaration and prop usage — do not alter other prop names or handler
signatures like onCategoryToggle, onEventSelect, or isLoading.
src/components/layout/header.tsx-63-80 (1)

63-80: ⚠️ Potential issue | 🟡 Minor

Expose the auto-refresh state with aria-pressed.

This control behaves like a toggle, but assistive tech currently only gets a changing label. Add aria-pressed={isAutoRefresh} so the current on/off state is announced.

As per coding guidelines, **/*.tsx: Review for accessibility (alt text, semantic HTML, ARIA labels), proper TypeScript typing, and no use of any type.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/header.tsx` around lines 63 - 80, The toggle button for
auto-refresh (Button wrapping RefreshCw) currently only updates aria-label;
update the Button element to include aria-pressed={isAutoRefresh} so screen
readers receive the boolean pressed state, keeping the existing aria-label and
onClick handler (onRefreshToggle) intact; ensure the prop uses the same
isAutoRefresh boolean and that TypeScript types accept the boolean ARIA prop on
the Button component.
src/__tests__/lib/format.test.ts-20-31 (1)

20-31: ⚠️ Potential issue | 🟡 Minor

The cases named here aren't actually exercised.

Line 21 still passes a full timestamp, so the “date without time component” path is untested, and Lines 28-30 never assert the time portion of formatDateTime. A formatter that drops time entirely would still pass this suite.

As per coding guidelines, **/*.test.ts: Review for test quality: meaningful assertions, edge cases covered, no flaky patterns, proper mocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/lib/format.test.ts` around lines 20 - 31, Update the tests so
they actually exercise the intended branches: for the formatDate test (function
name formatDate) pass a date-only string (e.g., "2025-12-25") instead of a full
timestamp so the no-time component path is exercised and still assert the
expected "Dec 25, 2025" pattern; for the formatDateTime test (function name
formatDateTime) assert the time portion as well (e.g., check for "14:30", "2:30
PM", or the specific time format your formatter returns) in addition to the
existing year and month checks so any implementation that drops or mangles the
time will fail.
src/app/globals.css-149-157 (1)

149-157: ⚠️ Potential issue | 🟡 Minor

Stylelint will fail on currentColor keyword casing.

Lines 149, 153, and 157 use currentColor; your configured rule expects lowercase currentcolor.

💡 Suggested fix
   0% {
-    box-shadow: 0 0 0 0 currentColor;
+    box-shadow: 0 0 0 0 currentcolor;
     opacity: 1;
   }
   70% {
-    box-shadow: 0 0 0 10px currentColor;
+    box-shadow: 0 0 0 10px currentcolor;
     opacity: 0;
   }
   100% {
-    box-shadow: 0 0 0 0 currentColor;
+    box-shadow: 0 0 0 0 currentcolor;
     opacity: 0;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/globals.css` around lines 149 - 157, The CSS uses the `currentColor`
keyword with uppercase C which violates stylelint's casing rule; update the
color keyword instances used in the `box-shadow` declarations (the three
occurrences shown in the keyframe steps 0%, 70%, 100%) to the lowercase form
`currentcolor` so they match the configured lint rule.
src/components/ui/command.tsx-165-165 (1)

165-165: ⚠️ Potential issue | 🟡 Minor

Use data-selected instead of data-checked for item selection state.

The check icon visibility is bound to group-data-[checked=true], but cmdk uses data-selected to indicate selection (focus) state on Command.Item. Change to group-data-[selected=true] so the icon displays when items are selected.

Reference: https://dip-cmdk.mintlify.app/components/item

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/command.tsx` at line 165, The CheckIcon's visibility class
is using group-data-[checked=true] but cmdk marks selected/focused Command.Item
via data-selected; update the className on the CheckIcon (the JSX inside
Command.Item) to use group-data-[selected=true] instead of
group-data-[checked=true] so the icon becomes visible when an item is selected.
src/components/ui/input-group.tsx-57-63 (1)

57-63: ⚠️ Potential issue | 🟡 Minor

Click-to-focus logic misses textarea controls and is easy to override accidentally.

Line 61 only targets input, so addons in textarea groups won’t focus the control. Also, because ...props is spread after onClick, caller-provided onClick replaces this behavior entirely.

💡 Suggested fix
 function InputGroupAddon({
   className,
   align = "inline-start",
-  ...props
+  onClick,
+  ...props
 }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
   return (
     <div
@@
-      onClick={(e) => {
-        if ((e.target as HTMLElement).closest("button")) {
+      onClick={(e) => {
+        onClick?.(e)
+        if (e.defaultPrevented) return
+        if (
+          e.target instanceof Element &&
+          e.target.closest("button, [role='button'], a, input, textarea, select")
+        ) {
           return
         }
-        e.currentTarget.parentElement?.querySelector("input")?.focus()
+        e.currentTarget.parentElement
+          ?.querySelector<HTMLElement>("input, textarea")
+          ?.focus()
       }}
       {...props}
     />
   )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/input-group.tsx` around lines 57 - 63, The click-to-focus
handler in src/components/ui/input-group.tsx currently only queries "input" and
is placed before the spread {...props} so callers can accidentally override it;
update the onClick on the wrapper used in the InputGroup component to (1) search
for broader focusable controls (e.g., querySelector with "input, textarea,
[contenteditable=true]" or a small helper that finds the first focusable form
control) instead of only "input" and (2) compose with any incoming onClick prop
rather than being replaced — either move {...props} before your onClick or call
props.onClick(e) inside the handler so caller clicks are preserved; reference
the existing onClick block and the spread of {...props} in the component when
making the change.
🧹 Nitpick comments (18)
src/components/ui/card.tsx (1)

36-47: Consider allowing semantic heading element for CardTitle.

CardTitle renders as a <div>, which lacks semantic meaning for assistive technologies. Screen reader users rely on heading elements for navigation. Consider accepting an as prop to allow consumers to specify the appropriate heading level.

♻️ Optional: Add polymorphic `as` prop for semantic flexibility
-function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+function CardTitle<T extends React.ElementType = "div">({
+  className,
+  as,
+  ...props
+}: { as?: T } & React.ComponentPropsWithoutRef<T>) {
+  const Component = as || "div"
   return (
-    <div
+    <Component
       data-slot="card-title"
       className={cn(
         "font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
         className
       )}
       {...props}
     />
   )
 }

Usage:

<CardTitle as="h2">Event Details</CardTitle>

Alternatively, the current pattern (delegating semantic responsibility to consumers via composition) is also valid for shadcn/ui-style libraries. As per coding guidelines, **/*.tsx files should be reviewed for semantic HTML.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/card.tsx` around lines 36 - 47, CardTitle currently always
renders a <div>, which is not semantic; update the CardTitle component to accept
an optional polymorphic as prop (e.g., as?: keyof JSX.IntrinsicElements |
React.ElementType) defaulting to "div", type props generically so props are
ComponentPropsWithRef<typeof As> and preserve className and {...props}, and
render the chosen element (const As = as ?? "div") while keeping
data-slot="card-title" and using cn for classes; ensure the component accepts
ref if applicable and export the updated signature so callers can use <CardTitle
as="h2">.
SECURITY.md (1)

11-16: Consider adding response time expectations and data handling policies.

The documented security practices are solid. For enhanced clarity and completeness, consider adding:

  • Expected response time for vulnerability reports
  • Data handling policy (particularly for NASA EONET API data)
  • HTTPS/TLS requirements for API communications
  • Security update policy (e.g., critical patches within 48h)
📋 Example enhancement
 ## Security Practices
 
 - No secrets or credentials are committed to this repository
 - Dependencies are scanned weekly via Dependabot and Trivy
 - Static analysis runs on every PR via Semgrep
 - Environment variables are used for all sensitive configuration
+- All external API communications use HTTPS/TLS
+- We aim to respond to vulnerability reports within 48 hours
+- Critical security patches are released within 7 days of disclosure
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SECURITY.md` around lines 11 - 16, Update the "Security Practices" section in
SECURITY.md to add explicit operational requirements: add a bullet specifying
expected response times for vulnerability reports (e.g., acknowledgement within
24h and remediation/triage SLA), a short data handling policy for NASA EONET API
data (storage, retention, access controls, and anonymization rules), a
requirement that all API communications use HTTPS/TLS, and a security update
policy stating timelines for critical/important patches (e.g., critical within
48h). Modify the existing "Security Practices" bullets under the "Security
Practices" heading to include these new points so reviewers can find them
alongside the current items about secrets, dependency scanning, and static
analysis.
.github/actions/security-scan/action.yml (1)

27-31: Consider guarding SARIF upload against missing file.

The upload step runs with if: always(), but if the Trivy step fails or is cancelled before writing the output file, the upload will error because trivy-results.sarif won't exist. You may want to add a file existence check or use if: always() && hashFiles('trivy-results.sarif') != ''.

♻️ Proposed fix
     - name: Upload Trivy results to GitHub Security
       uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
-      if: always()
+      if: always() && hashFiles('trivy-results.sarif') != ''
       with:
         sarif_file: trivy-results.sarif
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/actions/security-scan/action.yml around lines 27 - 31, The SARIF
upload step ("Upload Trivy results to GitHub Security") currently runs
unconditionally (if: always()) and will fail when trivy-results.sarif is
missing; change the step's condition to run only when the SARIF file exists (for
example replace or augment the if with a check like
hashFiles('trivy-results.sarif') != '' or a file-existence check) so the upload
action (uses: github/codeql-action/upload-sarif@...) is skipped when the Trivy
output was not produced.
src/components/map/map-provider.tsx (1)

33-39: Consider memoizing the context value to prevent unnecessary re-renders.

The context value object is recreated on every render of MapProvider, which can cause all consumers of useMap() to re-render even when none of the values have changed.

♻️ Proposed fix using useMemo
 import {
   createContext,
   useCallback,
   useContext,
+  useMemo,
   useState,
   type ReactNode,
 } from "react";

 // ... existing code ...

+  const value = useMemo(
+    () => ({ map, setMap, isLoaded, setIsLoaded: setIsLoadedCb }),
+    [map, setMap, isLoaded, setIsLoadedCb]
+  );
+
   return (
-    <MapContext.Provider
-      value={{ map, setMap, isLoaded, setIsLoaded: setIsLoadedCb }}
-    >
+    <MapContext.Provider value={value}>
       {children}
     </MapContext.Provider>
   );

As per coding guidelines, src/components/**: Review for memoization where appropriate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/map/map-provider.tsx` around lines 33 - 39, The context value
object passed into MapContext.Provider is recreated each render (value={{ map,
setMap, isLoaded, setIsLoaded: setIsLoadedCb }}), causing unnecessary consumer
re-renders; wrap the value in useMemo inside the MapProvider (memoize an object
containing map, setMap, isLoaded, setIsLoaded: setIsLoadedCb) with a dependency
array [map, setMap, isLoaded, setIsLoadedCb] and pass that memoized value to
MapContext.Provider so consumers only update when one of those actual values
changes.
src/lib/constants/categories.ts (1)

40-40: Strengthen CATEGORY_IDS typing to preserve category key safety.

Object.keys widens to string[]; narrowing to known keys improves downstream type checks.

Suggested typing refinement
-export const CATEGORY_IDS = Object.keys(CATEGORIES);
+export const CATEGORY_IDS = Object.keys(
+  CATEGORIES
+) as Array<keyof typeof CATEGORIES>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/constants/categories.ts` at line 40, CATEGORY_IDS is currently typed
as string[] because Object.keys widens to string[]; narrow it to the actual keys
of CATEGORIES by casting or using a typed helper so downstream code gets correct
keyof typing. Update the export of CATEGORY_IDS to use a type assertion like "as
Array<keyof typeof CATEGORIES>" (or use a generic keys<T>(obj: T) helper that
returns Array<keyof T>) so CATEGORY_IDS and references to CATEGORIES preserve
the specific category key types.
src/components/ui/dialog.tsx (1)

73-74: Consider adding aria-hidden to decorative icon.

The XIcon is decorative since the adjacent sr-only span provides the accessible name. Adding aria-hidden="true" ensures screen readers skip the icon entirely.

♿ Optional improvement
         <XIcon
+          aria-hidden="true"
         />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/dialog.tsx` around lines 73 - 74, The XIcon is decorative
and should be ignored by assistive tech; update the XIcon element in the dialog
component to include aria-hidden="true" (e.g., <XIcon aria-hidden="true" ... />)
so screen readers will skip it, leaving the adjacent sr-only span as the
accessible name.
.markdown-link-check.json (1)

10-14: Consider adding 172.16.0.0/12 private range to ignore patterns.

The configuration ignores 10.x and 192.168.x private networks but omits the 172.16.0.0/12 range (172.16.x.x through 172.31.x.x), which is also commonly used in internal/Docker networks.

Proposed addition
     {
       "pattern": "^https?://192\\.168\\."
+    },
+    {
+      "pattern": "^https?://172\\.(1[6-9]|2[0-9]|3[0-1])\\."
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.markdown-link-check.json around lines 10 - 14, Add a new ignore pattern to
.markdown-link-check.json to cover the 172.16.0.0/12 private range: introduce a
"pattern" entry that matches URLs starting with http(s)://172.16.x.x through
172.31.x.x (for example using the regex
"^https?://172\\.(1[6-9]|2[0-9]|3[0-1])\\."). Place this new pattern alongside
the existing "pattern" entries for 10.x and 192.168.x so the link checker also
skips the entire 172.16.0.0/12 range.
.github/copilot-instructions.md (1)

16-16: Clarify the src/-only rule to avoid blocking valid repo changes.

Line 16 is too absolute; this repository intentionally includes CI/docs/security/config code outside src/. Consider scoping this to application code only.

Proposed wording
-- All code in `src/` directory
+- Application/runtime code should live in `src/`; repository config/docs/workflow files may live outside `src/` as needed
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/copilot-instructions.md at line 16, The rule "All code in `src/`
directory" is too broad; update the wording to scope it to application source
code only (e.g., change the phrase "All code in `src/` directory" to
"Application source code should reside in the `src/` directory; CI, docs,
security, and config files may live outside `src/`") so the guideline permits
valid repo files outside src while still enforcing where application code
belongs.
src/app/page.tsx (1)

1-23: Consider adding metadata export for SEO.

This page is missing a metadata export, which is a Next.js App Router best practice for SEO and social sharing. The dashboard would benefit from proper page title and description.

📝 Suggested metadata export
 import { Suspense } from "react";
 import { DashboardShell } from "@/components/layout/dashboard-shell";
+import type { Metadata } from "next";
+
+export const metadata: Metadata = {
+  title: "Earth Events Dashboard",
+  description: "Real-time visualization of natural events from NASA EONET API",
+};

 function DashboardLoading() {

As per coding guidelines, src/app/**: "Review for Next.js App Router best practices: proper use of Server vs Client Components, metadata exports, loading/error boundaries, and data fetching patterns."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/page.tsx` around lines 1 - 23, Add a Next.js App Router metadata
export to this page by declaring and exporting a top-level metadata object
(e.g., export const metadata) that includes at minimum title and description for
the dashboard and optionally openGraph/social fields; place it alongside the
existing DashboardLoading and Home exports so the page uses the provided
title/description for SEO and social sharing. Ensure the metadata keys are plain
strings (title, description) and can include openGraph.title,
openGraph.description, and twitter.card if desired, without changing the
DashboardLoading component or the default Home export.
src/components/layout/mobile-drawer.tsx (2)

41-45: Consider controlling the active snap point.

activeSnapPoint={undefined} makes the drawer fully uncontrolled. If you need to programmatically control the drawer state (e.g., close it when an event is selected), you may want to lift this to controlled state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/mobile-drawer.tsx` around lines 41 - 45, The
Drawer.Root is currently uncontrolled because activeSnapPoint={undefined}; make
the drawer controlled by adding component state (e.g., useState for
activeSnapPoint) and pass that state into Drawer.Root's activeSnapPoint prop,
update it via the drawer's change callback (e.g., onActiveSnapPointChange or
similar event) and set the state to the desired snap (e.g., 0 to close) when
events occur (like an item selection) so you can programmatically open/close the
drawer.

13-38: Same unused categories prop as in Sidebar.

The categories: string[] prop (line 15) is declared in MobileDrawerProps but not destructured or used. Consider removing it for consistency with the fix in sidebar.tsx.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/mobile-drawer.tsx` around lines 13 - 38, The
MobileDrawerProps interface declares a categories: string[] prop that is never
used in the MobileDrawer component; remove the unused categories property from
MobileDrawerProps and from any places that pass it (or if callers rely on it,
instead add categories to the MobileDrawer parameter destructure and use it
consistently). Update the MobileDrawerProps interface and the MobileDrawer
function signature to match (remove categories from the type and from component
props) and adjust any callers that pass categories so types remain consistent.
src/components/events/event-timeline.tsx (1)

21-28: Consider a more stable key than array index.

Using index as the React key (line 28) can cause issues if the list changes. While the sorted array is derived and non-interactive, a composite key would be more robust.

♻️ Suggested stable key
-          <div key={index} className="relative">
+          <div key={`${geo.date}-${index}`} className="relative">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/events/event-timeline.tsx` around lines 21 - 28, Using the
array index as the React key in the sorted.map callback is unstable; change the
key on the returned <div> to a stable, unique identifier derived from the geo
item (for example use a composite like `${geo.type}-${geo.id ||
geo.properties?.id || geo.properties?.name}` or another unique field) inside the
sorted.map so that the mapping in the component (sorted.map, geo,
formatCoordinates) uses a deterministic key instead of index; ensure any
fallback uniqueness is consistent across renders.
src/app/layout.tsx (1)

39-41: overflow-hidden on body may affect accessibility in edge cases.

While intentional for a full-screen dashboard layout, overflow-hidden on <body> could prevent scrolling if content overflows unexpectedly (e.g., high zoom levels or dynamic content). Ensure internal scroll containers (like ScrollArea) handle all overflow scenarios.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/layout.tsx` around lines 39 - 41, The body has overflow-hidden which
can block scrolling in edge cases; remove or conditionalize the
"overflow-hidden" from the <body> class in layout.tsx and instead ensure an
internal scrollable container (e.g., wrap children in a dedicated ScrollArea or
a div with overflow-auto) inside NuqsAdapter or the main layout component so all
overflow is handled there; update NuqsAdapter or the immediate container around
{children} to provide proper focusable/keyboard accessible scroll behavior and
test zoom/dynamic-content scenarios to confirm scrolling still works.
src/components/layout/dashboard-shell.tsx (2)

63-70: Unchecked type assertion on URL-sourced status value.

The status as "open" | "closed" | "all" assertion assumes the value from URL parameters is always valid. Malformed or tampered URLs could pass invalid values.

Consider validating before casting, or handle this validation in useFilters.

🛡️ Defensive validation
  const handleStatusChange = useCallback(
    (status: string) => {
+     const validStatuses = ["open", "closed", "all"] as const;
+     if (!validStatuses.includes(status as typeof validStatuses[number])) {
+       return;
+     }
      void setFilters({
        status: status as "open" | "closed" | "all",
      });
    },
    [setFilters],
  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/dashboard-shell.tsx` around lines 63 - 70, The handler
handleStatusChange currently force-casts the URL-derived status with `status as
"open" | "closed" | "all"`, which is unsafe; update handleStatusChange (or add a
small validator used by useFilters) to check the incoming status string against
the allowed set ["open","closed","all"] and only call setFilters with a
validated value (or a safe fallback like "all") — e.g., perform a runtime
check/guard inside handleStatusChange and avoid the unchecked type assertion so
malformed/tampered URL values are rejected or normalized before calling
setFilters.

49-50: Unsafe type cast bypasses TypeScript checks.

The as unknown as GeoJSON.FeatureCollection double-cast completely bypasses type safety. If geojsonData has an unexpected structure, runtime errors could occur.

Consider defining a proper return type for useGeoJSON or adding runtime validation.

♻️ Suggested approach

Either update useGeoJSON to return a properly typed response, or add a type guard:

function isFeatureCollection(data: unknown): data is GeoJSON.FeatureCollection {
  return (
    data !== null &&
    typeof data === "object" &&
    "type" in data &&
    (data as { type: unknown }).type === "FeatureCollection"
  );
}

const geojsonFeatureCollection = isFeatureCollection(geojsonData) ? geojsonData : null;

As per coding guidelines: **/*.tsx: no use of any type (the double-cast effectively achieves the same unsafe behavior).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/dashboard-shell.tsx` around lines 49 - 50, The
double-cast to GeoJSON.FeatureCollection is unsafe; replace it by either
updating useGeoJSON to return a properly typed GeoJSON.FeatureCollection (or
union) or add a runtime type guard (e.g., isFeatureCollection) and use that to
set geojsonFeatureCollection instead of the as unknown as cast; locate the
assignment to geojsonFeatureCollection and the useGeoJSON hook to implement one
of these fixes and ensure no use of any/unsafe casts remain.
.pre-commit-config.yaml (1)

27-61: Local hooks require pre-installed system tools.

The local hooks for gitleaks, shellcheck, shellharden, prettier, eslint, and zizmor use language: system, requiring these tools to be installed on each contributor's machine. Consider documenting these requirements in a CONTRIBUTING.md or the README to help new contributors set up their environment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.pre-commit-config.yaml around lines 27 - 61, Add documentation listing the
system tools required by the local pre-commit hooks (gitleaks, shellcheck,
shellharden, prettier, eslint, and zizmor) and how to install or configure them;
update CONTRIBUTING.md or README with a new "Development setup" or "Pre-commit
hooks" section that enumerates each tool, required versions (if any),
installation commands for major platforms, and how to verify the tool is
available so contributors understand why the hooks use language: system and how
to satisfy those dependencies.
src/lib/api/eonet-client.ts (1)

8-8: Don't make the reusable client bypass your own proxy layer.

Every function targets NASA directly, so importing this module from UI code would skip the /api/eonet/* caching/rate-limit protection described in the PR and could end up shipping direct upstream requests to the browser. Either point this helper at the internal API routes or mark the module server-only to prevent accidental client-side use.

As per coding guidelines, src/lib/**: Review for proper TypeScript typing, error handling, and separation of concerns. API clients should handle errors gracefully.

Also applies to: 33-63

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/api/eonet-client.ts` at line 8, The module currently sets BASE_URL to
the public NASA endpoint which lets callers (including client-side code) bypass
your internal /api/eonet/* proxy; change BASE_URL to the internal route (e.g.,
"/api/eonet") or mark the module server-only (add the appropriate server-only
directive) so it cannot be imported from the browser, and while here improve
error handling and typings in the exported functions (e.g., the fetch wrappers
and any functions that reference BASE_URL) so they return typed results or
throw/catch errors consistently rather than allowing unhandled fetch failures.
src/components/events/event-detail-panel.tsx (1)

75-83: Replace inline category styling with classes or tokens.

The badge and dot both rely on style={...}, which breaks the repo rule for component styling and makes them harder to theme consistently. Push the color token/class into getCategoryConfig() and render classes here instead.

As per coding guidelines, src/components/**: Review for React best practices: proper prop typing, accessibility (aria labels, keyboard navigation), memoization where appropriate, and no inline styles.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/events/event-detail-panel.tsx` around lines 75 - 83, The Badge
and the inner dot in event-detail-panel.tsx currently use inline style props;
update getCategoryConfig() to return a color token or CSS class name (e.g.,
badgeClass and dotClass or colorToken) instead of raw color values, then remove
style={...} from the Badge and span and apply those returned classes via
className (or data- attributes if using CSS variables) so theming follows repo
rules; also ensure getCategoryConfig() typing is updated to include the new
fields and preserve accessibility by keeping existing text content/ARIA on the
Badge.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 940fb1e3-6218-47e4-ac39-21fc431b0d4d

📥 Commits

Reviewing files that changed from the base of the PR and between b71b970 and 693426b.

📒 Files selected for processing (87)
  • .claude/hooks/post-edit.sh
  • .claude/settings.json
  • .coderabbit.yaml
  • .github/ISSUE_TEMPLATE/bug-report.md
  • .github/ISSUE_TEMPLATE/feature-request.md
  • .github/PULL_REQUEST_TEMPLATE.md
  • .github/actions/security-scan/action.yml
  • .github/actions/update-pre-commit-composite/action.yml
  • .github/copilot-instructions.md
  • .github/dependabot.yml
  • .github/scripts/check-readme.sh
  • .github/scripts/validate-structure.sh
  • .github/workflows/ci.yml
  • .github/workflows/quality-checks.yml
  • .github/workflows/security.yml
  • .github/workflows/update-pre-commit-hooks.yml
  • .gitignore
  • .markdown-link-check.json
  • .markdownlint.yaml
  • .pre-commit-config.yaml
  • .prettierignore
  • .prettierrc
  • .secrets.baseline
  • .yamllint.yml
  • AGENTS.md
  • CLAUDE.md
  • CODEOWNERS
  • CONTRIBUTING.md
  • LICENSE
  • README.md
  • SECURITY.md
  • docs/adr/001-nextjs-over-spa.md
  • docs/adr/002-raw-mapbox-over-react-map-gl.md
  • docs/adr/003-url-params-for-filter-state.md
  • eslint.config.mjs
  • src/__tests__/lib/format.test.ts
  • src/app/api/eonet/categories/route.ts
  • src/app/api/eonet/events/route.ts
  • src/app/api/eonet/geojson/route.ts
  • src/app/api/eonet/sources/route.ts
  • src/app/globals.css
  • src/app/layout.tsx
  • src/app/page.tsx
  • src/components/events/event-card.tsx
  • src/components/events/event-detail-panel.tsx
  • src/components/events/event-list.tsx
  • src/components/events/event-timeline.tsx
  • src/components/filters/category-filter.tsx
  • src/components/filters/date-range-filter.tsx
  • src/components/filters/source-filter.tsx
  • src/components/filters/status-filter.tsx
  • src/components/layout/dashboard-shell.tsx
  • src/components/layout/header.tsx
  • src/components/layout/mobile-drawer.tsx
  • src/components/layout/sidebar.tsx
  • src/components/map/event-markers.tsx
  • src/components/map/event-popup.tsx
  • src/components/map/map-container.tsx
  • src/components/map/map-controls.tsx
  • src/components/map/map-provider.tsx
  • src/components/map/storm-tracks.tsx
  • src/components/stats/stat-card.tsx
  • src/components/stats/stats-bar.tsx
  • src/components/ui/badge.tsx
  • src/components/ui/card.tsx
  • src/components/ui/command.tsx
  • src/components/ui/dialog.tsx
  • src/components/ui/input-group.tsx
  • src/components/ui/input.tsx
  • src/components/ui/scroll-area.tsx
  • src/components/ui/separator.tsx
  • src/components/ui/sheet.tsx
  • src/components/ui/skeleton.tsx
  • src/components/ui/textarea.tsx
  • src/components/ui/toggle-group.tsx
  • src/components/ui/toggle.tsx
  • src/lib/api/eonet-client.ts
  • src/lib/api/types.ts
  • src/lib/constants/categories.ts
  • src/lib/constants/map-config.ts
  • src/lib/hooks/use-categories.ts
  • src/lib/hooks/use-events.ts
  • src/lib/hooks/use-filters.ts
  • src/lib/hooks/use-geojson.ts
  • src/lib/utils/format.ts
  • vitest.config.ts
  • zizmor.yml
💤 Files with no reviewable changes (1)
  • AGENTS.md

Comment thread .github/workflows/ci.yml
Comment on lines +48 to +53
- uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: node_modules
key: node-modules-${{ hashFiles('package-lock.json') }}

- run: npx eslint .

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and read the workflow file
find . -name "ci.yml" -path ".github/workflows/*"

Repository: gamaware/earth-events-dashboard

Length of output: 57


🏁 Script executed:

# Read the workflow file to see the full context
cat -n .github/workflows/ci.yml | head -120

Repository: gamaware/earth-events-dashboard

Length of output: 4013


🏁 Script executed:

# Check lines 48-53 specifically
sed -n '48,53p' .github/workflows/ci.yml | cat -n

Repository: gamaware/earth-events-dashboard

Length of output: 338


🏁 Script executed:

# Check all other mentioned lines (69-74, 90-95, 111-116)
echo "=== Lines 69-74 ===" && sed -n '69,74p' .github/workflows/ci.yml | cat -n
echo "=== Lines 90-95 ===" && sed -n '90,95p' .github/workflows/ci.yml | cat -n
echo "=== Lines 111-116 ===" && sed -n '111,116p' .github/workflows/ci.yml | cat -n

Repository: gamaware/earth-events-dashboard

Length of output: 935


🌐 Web query:

GitHub Actions actions/cache/restore cache-hit output

💡 Result:

The actions/cache/restore action in GitHub Actions provides a cache-hit output that indicates whether an exact match was found for the provided primary key. - If an exact match for the key is found (cache hit), cache-hit is set to the string 'true'. - If a cache is restored using one of the restore-keys (partial match), cache-hit is set to the string 'false'. - If no matching cache is found (cache miss), cache-hit is set to the string 'false' (updated behavior in recent versions like v4.1.0+; previously an empty string in some cases). This output is a string (not a boolean), so use comparisons like steps.cache.outputs.cache-hit != 'true' in if conditions to skip steps on exact hit. Example usage: - name: Cache Restore id: cache-restore uses: actions/cache/restore@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - name: Install if miss if: steps.cache-restore.outputs.cache-hit != 'true' run: npm ci Note: Official docs sometimes describe it as "boolean", but implementation and README confirm it's a string with values 'true', 'false', or '' (older behavior).

Citations:


Add cache-miss fallback to all restore steps.

These jobs restore node_modules but have no recovery when the cache misses. If the cache expires or is evicted, npm run build, npx eslint ., npx tsc --noEmit, and npx vitest run will fail without dependencies.

🛠️ Add cache-hit check and fallback to npm ci
+      - id: restore-node-modules
         uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
         with:
           path: node_modules
           key: node-modules-${{ hashFiles('package-lock.json') }}
+      - if: steps.restore-node-modules.outputs.cache-hit != 'true'
+        run: npm ci

Apply to: lines 48–53 (lint), 69–74 (type-check), 90–95 (test), 111–116 (build).

📝 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
- uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: node_modules
key: node-modules-${{ hashFiles('package-lock.json') }}
- run: npx eslint .
- id: restore-node-modules
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: node_modules
key: node-modules-${{ hashFiles('package-lock.json') }}
- if: steps.restore-node-modules.outputs.cache-hit != 'true'
run: npm ci
- run: npx eslint .
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 48 - 53, Add a cache-miss fallback for
each node_modules restore step: give the restore step an id (e.g., id:
cache-node-modules), check its outputs.cache-hit, and when the value is not
"true" run an install (npm ci) before running the tasks; apply this pattern to
the steps that run "npx eslint .", "npx tsc --noEmit", "npx vitest run", and
"npm run build" so they always have dependencies if the cache missed.

Comment on lines +156 to +164
- name: Install zizmor
env:
ZIZMOR_VERSION: "1.23.1"
run: |
tmpdir="$(mktemp -d)"
curl -fsL \
"https://github.com/woodruffw/zizmor/releases/download/v${ZIZMOR_VERSION}/zizmor-x86_64-unknown-linux-gnu.tar.gz" \
| tar xz -C "$tmpdir"
sudo mv "$tmpdir/zizmor" /usr/local/bin/zizmor

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/quality-checks.yml | sed -n '150,180p'

Repository: gamaware/earth-events-dashboard

Length of output: 1379


🏁 Script executed:

# Check if there's a zizmor GitHub Action available
web_search

Repository: gamaware/earth-events-dashboard

Length of output: 125


🌐 Web query:

zizmor GitHub releases checksum verification security

💡 Result:

zizmor is a Rust-based static analysis tool for detecting security vulnerabilities in GitHub Actions workflows, such as template injections, credential leaks, excessive permissions, and artifact integrity issues without checksum verification. It has 76 releases on GitHub (latest v1.23.1 as of March 2026), with pre-built binaries available (e.g., for Linux, macOS, Windows). Official installation prefers package managers like Homebrew, PyPI, Cargo, or Docker for trusted distribution; direct GitHub binary downloads are provided on a best-effort basis with potential compatibility issues. No checksums (SHA256) are published on release pages, but GitHub verifies commit signatures (GPG key B5690EEEBB952194). CI workflows generate SHA256 digests for artifacts (e.g., v1.18.0 Linux: sha256:93679bc72a4bff950f89dbaf380c051dc57a4a24255953df684fd716fe18a87f), verifiable post-download using tools like sha256sum. zizmor itself audits workflows for missing checksums on downloads (e.g., artifacts, actions), recommending pinning and verification to prevent supply chain attacks, as noted in research papers and issues like pypa/cibuildwheel#2770.

Citations:


🌐 Web query:

zizmor GitHub Action official tool

💡 Result:

The official GitHub Action for zizmor, a static analysis tool for GitHub Actions workflows, is zizmorcore/zizmor-action. It runs zizmor directly in GitHub Actions workflows, integrates with GitHub Advanced Security for SARIF output and code scanning, and is available on the GitHub Marketplace. Latest release: v0.5.2 (March 2026). Official docs link to it at https://docs.zizmor.sh/integrations/#github-actions. Example usage: - name: Run zizmor uses: zizmorcore/zizmor-action@v0.5.2 zizmor core repo: https://github.com/zizmorcore/zizmor (latest v1.23.1, March 2026).

Citations:


🌐 Web query:

GitHub Actions security best practices supply chain binary installation

💡 Result:

GitHub Actions security best practices for supply chain security, particularly for binary installations, focus on preventing tampering, injection attacks, and unauthorized access. Key recommendations from official GitHub documentation and trusted sources include: 1. Pin third-party actions to full-length commit SHAs (not tags like v1 or latest) to ensure immutability and prevent supply chain attacks where tags are hijacked. Example: - uses: actions/checkout@192a8f9 # v4.1.7 2. Use trusted, pinned actions for binary installation, such as step-security/action-install-gh-release@v2, which securely downloads binaries from GitHub Releases by matching platform/architecture, verifying digests if provided, and supporting caching. Avoid direct curl | bash or unverified downloads from external URLs. Example: - name: Install tool uses: step-security/action-install-gh-release@v2 with: repo: owner/repo tag: v1.0.0 digest: sha256:abc123... # Optional for integrity 3. Verify binary integrity using checksums or artifact attestations. Generate attestations with actions/attest@v4 for provenance (SLSA Level 2+), and verify with gh attestation verify path/to/binary -R owner/repo. 4. Minimize GITHUB_TOKEN permissions to read-only where possible, use OIDC for cloud auth instead of long-lived secrets, and mask/add-mask sensitive data. 5. Mitigate script injection: Avoid using GitHub context (e.g., ${{ github.event.inputs }}) directly in run: | scripts; use env vars or dedicated actions instead. 6. Use ephemeral GitHub-hosted runners, enable CodeQL/code scanning for workflows, OpenSSF Scorecards, and Dependabot for action updates. 7. For runtime: Add step-security/harden-runner to jobs to restrict egress and secure the environment. 8. Prefer GitHub Packages or Releases for binaries, use immutable releases, and generate SBOMs. These practices harden the supply chain by ensuring reproducibility, verifiability, and least privilege. Enable repository/org policies to enforce pinning and review third-party actions.

Citations:


Use the official zizmorcore/zizmor-action or verify the binary digest before executing it.

This step downloads a release tarball via curl | tar and executes the extracted binary with no checksum verification. GitHub Actions security best practices recommend verifying binary integrity using checksums or attestations to prevent supply-chain attacks.

Two solutions: (1) Switch to the official GitHub Action zizmorcore/zizmor-action@v0.5.2, which handles binary verification and is recommended in the official zizmor documentation. (2) Alternatively, download the binary using step-security/action-install-gh-release with digest verification, or manually verify the SHA256 digest from the zizmor CI artifacts before moving the binary into $PATH.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/quality-checks.yml around lines 156 - 164, The current
"Install zizmor" step downloads and installs a release tarball without integrity
checks; replace this raw curl|tar install with the official action
zizmorcore/zizmor-action@v0.5.2 (update the step name and remove the manual curl
logic) OR add digest verification: fetch the tarball and the corresponding
SHA256 from the release metadata, verify the checksum against ZIZMOR_VERSION
before moving the binary, and fail the job on mismatch; refer to the step's env
variable ZIZMOR_VERSION and the step name "Install zizmor" to locate and update
the code.

Comment on lines +13 to +25
update-hooks:
name: Update Pre-commit Hooks
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Update Hooks
uses: ./.github/actions/update-pre-commit-composite
with:
github_token: ${{ secrets.PRE_COMMIT_PAT }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use a dedicated environment for PAT-backed automation.

Line 25 consumes secrets.PRE_COMMIT_PAT without an environment, which matches the zizmor warning and weakens secret governance.

Proposed fix
 jobs:
   update-hooks:
     name: Update Pre-commit Hooks
     runs-on: ubuntu-latest
+    environment: pre-commit-automation
     steps:
       - name: Checkout
         uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
         with:
           persist-credentials: false

As per coding guidelines, .github/workflows/**: Review for GitHub Actions security best practices, pinned action versions, and least-privilege permissions.

📝 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
update-hooks:
name: Update Pre-commit Hooks
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Update Hooks
uses: ./.github/actions/update-pre-commit-composite
with:
github_token: ${{ secrets.PRE_COMMIT_PAT }}
update-hooks:
name: Update Pre-commit Hooks
runs-on: ubuntu-latest
environment: pre-commit-automation
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Update Hooks
uses: ./.github/actions/update-pre-commit-composite
with:
github_token: ${{ secrets.PRE_COMMIT_PAT }}
🧰 Tools
🪛 GitHub Actions: Quality Checks

[warning] 25-25: zizmor warning[secrets-outside-env]: secrets referenced without a dedicated environment. secret is accessed outside of a dedicated environment (audit confidence: High).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/update-pre-commit-hooks.yml around lines 13 - 25, The
workflow step "Update Hooks" currently passes github_token: ${{
secrets.PRE_COMMIT_PAT }} without an environment; update the "update-hooks"
job/step to use a dedicated environment (e.g., environment: pre-commit or
another dedicated environment name) so the PAT is consumed under environment
protection, and ensure the step that uses
./.github/actions/update-pre-commit-composite still receives the github_token
input (github_token: ${{ secrets.PRE_COMMIT_PAT }}) but only inside that
environment block; keep the job name "update-hooks" and the step "Update Hooks"
unchanged otherwise to preserve behavior.

Comment thread .secrets.baseline
Comment on lines +126 to +129
"pattern": [
"node_modules/.*",
".next/.*",
"package-lock.json"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
import re, json
from pathlib import Path

data = json.loads(Path(".secrets.baseline").read_text())
patterns = []
for f in data.get("filters_used", []):
    if f.get("path") == "detect_secrets.filters.regex.should_exclude_file":
        patterns.extend(f.get("pattern", []))

tests = [
    ".next/static/chunk.js",
    "anext/static/chunk.js",
    "xnext/static/chunk.js",
]

for p in patterns:
    rx = re.compile(p)
    print(f"\npattern: {p}")
    for t in tests:
        print(f"  {t:<28} -> {bool(rx.search(t))}")
PY

Repository: gamaware/earth-events-dashboard

Length of output: 504


🏁 Script executed:

cd /root/repo && python - <<'PY'
import json
from pathlib import Path

data = json.loads(Path(".secrets.baseline").read_text())
patterns = []
for f in data.get("filters_used", []):
    if f.get("path") == "detect_secrets.filters.regex.should_exclude_file":
        patterns = f.get("pattern", [])
        break

print("All exclusion patterns in .secrets.baseline:")
for i, p in enumerate(patterns, 1):
    print(f"{i}. {p}")
PY

Repository: gamaware/earth-events-dashboard

Length of output: 135


🏁 Script executed:

python - <<'PY'
import json
from pathlib import Path

data = json.loads(Path(".secrets.baseline").read_text())
patterns = []
for f in data.get("filters_used", []):
    if f.get("path") == "detect_secrets.filters.regex.should_exclude_file":
        patterns = f.get("pattern", [])
        break

print("All exclusion patterns in .secrets.baseline:")
for i, p in enumerate(patterns, 1):
    print(f"{i}. {p}")
PY

Repository: gamaware/earth-events-dashboard

Length of output: 171


Escape . in .next/.* regex pattern to prevent unintended matches.

Line 128: The pattern .next/.* has an unescaped dot before next, which matches any character (including anext/, xnext/, etc.) rather than only the literal .next/ directory. This can cause legitimate files to be incorrectly excluded from secret scanning.

Proposed fix
-        ".next/.*",
+        "\\.next/.*",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.secrets.baseline around lines 126 - 129, The regex pattern entry ".next/.*"
in the "pattern" array should escape the dot so it matches the literal directory
name; update the pattern string in the "pattern" array from ".next/.*" to
"\.next/.*" (i.e., escape the leading dot) so the secret scanner only excludes
the actual .next directory rather than any character followed by "next/".

Comment thread SECURITY.md
Comment on lines +7 to +9
1. Do **not** open a public issue
2. Email the maintainer with details of the vulnerability
3. Include steps to reproduce if possible

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Provide a specific email address or contact method for vulnerability reports.

Line 8 instructs reporters to "Email the maintainer" but provides no email address or contact information. Without a specific contact method, security researchers cannot actually report vulnerabilities, making this security policy ineffective.

🔒 Proposed fix: Add contact information

Option 1 (Recommended): Add a specific email address:

 1. Do **not** open a public issue
-2. Email the maintainer with details of the vulnerability
+2. Email security@yourdomain.com with details of the vulnerability
 3. Include steps to reproduce if possible

Option 2: Reference GitHub's private vulnerability reporting (if enabled):

 1. Do **not** open a public issue
-2. Email the maintainer with details of the vulnerability
+2. Report privately via GitHub Security Advisories or email MAINTAINER_EMAIL
 3. Include steps to reproduce if possible

Replace MAINTAINER_EMAIL with an actual email address (e.g., the repository owner's contact email).

📝 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
1. Do **not** open a public issue
2. Email the maintainer with details of the vulnerability
3. Include steps to reproduce if possible
1. Do **not** open a public issue
2. Email security@yourdomain.com with details of the vulnerability
3. Include steps to reproduce if possible
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SECURITY.md` around lines 7 - 9, Update the SECURITY.md instruction "Email
the maintainer with details of the vulnerability" to include a concrete contact
method: add a specific email address (replace MAINTAINER_EMAIL with the real
address) or, alternatively, reference the repository's GitHub private
vulnerability reporting link (e.g., "Report via GitHub Security Advisories:
https://github.com/OWNER/REPO/security") so reporters have a usable channel to
submit vulnerability reports.

Comment on lines +51 to +64
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn(
"top-1/3 translate-y-0 overflow-hidden rounded-xl! p-0",
className
)}
showCloseButton={showCloseButton}
>
{children}
</DialogContent>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Dialog title/description should be inside DialogContent for proper labeling.

DialogTitle and DialogDescription are rendered outside DialogContent (Line 52-55), which can break ARIA association for the modal content.

💡 Suggested fix
   return (
     <Dialog {...props}>
-      <DialogHeader className="sr-only">
-        <DialogTitle>{title}</DialogTitle>
-        <DialogDescription>{description}</DialogDescription>
-      </DialogHeader>
       <DialogContent
         className={cn(
           "top-1/3 translate-y-0 overflow-hidden rounded-xl! p-0",
           className
         )}
         showCloseButton={showCloseButton}
       >
+        <DialogHeader className="sr-only">
+          <DialogTitle>{title}</DialogTitle>
+          <DialogDescription>{description}</DialogDescription>
+        </DialogHeader>
         {children}
       </DialogContent>
     </Dialog>
   )
 }

As per coding guidelines, **/*.tsx: Review for accessibility (alt text, semantic HTML, ARIA labels), proper TypeScript typing, and no use of any type.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/command.tsx` around lines 51 - 64, The DialogTitle and
DialogDescription are currently rendered outside DialogContent which breaks ARIA
association; move the DialogHeader (or at minimum the DialogTitle and
DialogDescription) inside DialogContent so that DialogContent contains the
labeling elements. Update the JSX in the command component so DialogContent
wraps the title/description and children while preserving props like className
and showCloseButton (keep Dialog, DialogHeader, DialogContent, DialogTitle,
DialogDescription as identifiers to locate the changes) and ensure any sr-only
class is preserved if the header should remain visually hidden for
accessibility.

Comment on lines +69 to +83
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div data-slot="command-input-wrapper" className="p-1 pb-0">
<InputGroup className="h-8! rounded-lg! border-input/30 bg-input/30 shadow-none! *:data-[slot=input-group-addon]:pl-2!">
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Provide a default accessible name for CommandInput.

This wrapper can render an unlabeled input when consumers don’t pass aria-label. Add a fallback (e.g., "Search commands").

💡 Suggested fix
 function CommandInput({
   className,
   ...props
 }: React.ComponentProps<typeof CommandPrimitive.Input>) {
+  const ariaLabel = props["aria-label"] ?? "Search commands"
   return (
     <div data-slot="command-input-wrapper" className="p-1 pb-0">
       <InputGroup className="h-8! rounded-lg! border-input/30 bg-input/30 shadow-none! *:data-[slot=input-group-addon]:pl-2!">
         <CommandPrimitive.Input
           data-slot="command-input"
+          aria-label={ariaLabel}
           className={cn(
             "w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
             className
           )}
           {...props}
         />

As per coding guidelines, src/components/**: Review for React best practices: proper prop typing, accessibility (aria labels, keyboard navigation), memoization where appropriate, and no inline styles.

📝 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
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div data-slot="command-input-wrapper" className="p-1 pb-0">
<InputGroup className="h-8! rounded-lg! border-input/30 bg-input/30 shadow-none! *:data-[slot=input-group-addon]:pl-2!">
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
const ariaLabel = props["aria-label"] ?? "Search commands"
return (
<div data-slot="command-input-wrapper" className="p-1 pb-0">
<InputGroup className="h-8! rounded-lg! border-input/30 bg-input/30 shadow-none! *:data-[slot=input-group-addon]:pl-2!">
<CommandPrimitive.Input
data-slot="command-input"
aria-label={ariaLabel}
className={cn(
"w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/command.tsx` around lines 69 - 83, The CommandInput
component can render an unlabeled input; update CommandInput to ensure an
accessible name by reading aria-label from props and providing a fallback (e.g.,
"Search commands") when none is passed, and then pass that aria-label into the
CommandPrimitive.Input call; locate the CommandInput function and the
CommandPrimitive.Input usage to implement this override while preserving
existing prop typing (React.ComponentProps<typeof CommandPrimitive.Input>) and
spreading other props as before.

Comment on lines +6 to +20
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}

export { Input }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify wrapper shape and check whether consumers already pass refs.
rg -n 'forwardRef|function Input\(' src/components/ui/input.tsx
rg -nP --type=tsx '<Input[^>]*\bref=' src

Repository: gamaware/earth-events-dashboard

Length of output: 200


🏁 Script executed:

#!/bin/bash
# Search for Input component usages with ref attribute (without tsx type)
rg -n '<Input[^>]*\bref=' src/ || echo "No ref usages found"

# Find InputPrimitive definition
rg -n 'const InputPrimitive\|export.*InputPrimitive' src/components/ui/ | head -20

# Check full input.tsx to understand imports and component structure
cat -n src/components/ui/input.tsx

Repository: gamaware/earth-events-dashboard

Length of output: 1274


🏁 Script executed:

#!/bin/bash
# Check if there are other forwardRef usages to compare patterns
rg -n 'React\.forwardRef|forwardRef' src/components/ | head -20

# Check for other UI wrapper components to see if they use forwardRef
rg -n 'export.*function.*\{|const.*=.*\(' src/components/ui/*.tsx | grep -E 'function|const' | head -30

# Check package.json to see base-ui version
cat package.json | grep -A 2 -B 2 'base-ui'

Repository: gamaware/earth-events-dashboard

Length of output: 612


🏁 Script executed:

#!/bin/bash
# Check all wrapper components in ui folder and their patterns
ls -la src/components/ui/*.tsx | wc -l
echo "---"
# Look for any components that might wrap primitives or external components
rg -n 'React.ComponentProps|ComponentPropsWithoutRef|ElementRef' src/components/ui/ | head -20

# Check if there are any ref-related type definitions in the project
rg -n 'forwardRef|ElementRef|useRef' src/components/ | head -20

Repository: gamaware/earth-events-dashboard

Length of output: 3104


🏁 Script executed:

#!/bin/bash
# Check textarea for the same pattern
cat -n src/components/ui/textarea.tsx

# Check if there are any other input/textarea-like components
rg -n 'InputPrimitive|TextareaPrimitive' src/components/ui/

Repository: gamaware/earth-events-dashboard

Length of output: 1185


Forward the input ref to preserve form/focus interoperability.

The Input component is a plain function that cannot forward refs to the underlying InputPrimitive. Parent components passing ref={inputRef} will have their refs silently ignored, breaking ref access needed for form integration and focus management.

Use React.forwardRef with proper typing to accept and forward refs:

Proposed fix
-function Input({ className, type, ...props }: React.ComponentProps<"input">) {
-  return (
-    <InputPrimitive
-      type={type}
-      data-slot="input"
-      className={cn(
-        "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
-        className
-      )}
-      {...props}
-    />
-  )
-}
+const Input = React.forwardRef<
+  React.ElementRef<typeof InputPrimitive>,
+  React.ComponentPropsWithoutRef<typeof InputPrimitive>
+>(({ className, type, ...props }, ref) => {
+  return (
+    <InputPrimitive
+      ref={ref}
+      type={type}
+      data-slot="input"
+      className={cn(
+        "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
+        className
+      )}
+      {...props}
+    />
+  )
+})
+Input.displayName = "Input"
📝 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
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }
const Input = React.forwardRef<
React.ElementRef<typeof InputPrimitive>,
React.ComponentPropsWithoutRef<typeof InputPrimitive>
>(({ className, type, ...props }, ref) => {
return (
<InputPrimitive
ref={ref}
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
})
Input.displayName = "Input"
export { Input }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/input.tsx` around lines 6 - 20, The Input component
currently ignores refs; update it to use React.forwardRef to accept a ref
parameter and forward it to the underlying InputPrimitive (preserve its props
typing by using React.ComponentPropsWithRef<"input"> or appropriate prop type),
ensure the forwarded ref is passed as ref={ref} to InputPrimitive, update the
function name to a const assigned to forwardRef (e.g., const Input =
React.forwardRef<HTMLInputElement,
React.ComponentPropsWithoutRef<"input">>(...), and keep the existing className
merging and props spread, then export the forwarded component as before.

Comment on lines +45 to +46
"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] rounded-lg data-[size=sm]:rounded-[min(var(--radius-md),10px)] data-vertical:flex-col data-vertical:items-stretch",
className

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
# Verify whether custom Tailwind variants exist for data-vertical/horizontal patterns.
set -euo pipefail

echo "Searching for custom variant declarations..."
rg -n "custom-variant|data-vertical|data-horizontal|group-data-vertical|group-data-horizontal|data-\\[orientation="

echo
echo "Searching toggle-group usage of orientation selectors..."
rg -n "data-vertical:|group-data-vertical|group-data-horizontal|data-orientation" src/components/ui/toggle-group.tsx

Repository: gamaware/earth-events-dashboard

Length of output: 3047


🏁 Script executed:

#!/bin/bash
# Check the full toggle-group component to see what attributes are actually set
echo "=== Full toggle-group.tsx content ==="
cat -n src/components/ui/toggle-group.tsx

echo
echo "=== Check for Tailwind config files ==="
fd -e "tailwind.config" -e "tailwind.config.ts" -e "tailwind.config.js"

echo
echo "=== Check CSS config in globals.css ==="
head -50 src/app/globals.css

Repository: gamaware/earth-events-dashboard

Length of output: 5943


🏁 Script executed:

#!/bin/bash
# Check scroll-area to see what attributes it emits vs what selectors it uses
echo "=== scroll-area.tsx showing data attributes and selectors ==="
grep -n "data-" src/components/ui/scroll-area.tsx | head -20

Repository: gamaware/earth-events-dashboard

Length of output: 610


🏁 Script executed:

#!/bin/bash
echo "=== separator.tsx data attribute and selector patterns ==="
grep -n "data-" src/components/ui/separator.tsx

Repository: gamaware/earth-events-dashboard

Length of output: 287


🏁 Script executed:

#!/bin/bash
echo "=== Full separator.tsx ==="
cat -n src/components/ui/separator.tsx

Repository: gamaware/earth-events-dashboard

Length of output: 821


🏁 Script executed:

#!/bin/bash
# Search for custom Tailwind plugin definitions, config files, or variant handlers
echo "=== Search for Tailwind plugin or variant configuration ==="
fd -e "tailwind" -e "plugin" -e "config" -type f | head -20

echo
echo "=== Search for PostCSS or any config files ==="
fd -e "postcss.config" -e "postcss.config.js" -e "postcss.config.ts" -e "next.config" -type f

echo
echo "=== Check if there are any plugin definitions in globals.css or other CSS files ==="
rg -n "@layer|plugin|variant" src/app/

echo
echo "=== Check all CSS files ==="
fd -e "css" src/ --type f

Repository: gamaware/earth-events-dashboard

Length of output: 1075


🏁 Script executed:

#!/bin/bash
echo "=== Files in repository root ==="
ls -la | grep -E "tailwind|next|postcss"

echo
echo "=== Check package.json for tailwind config ==="
cat package.json | grep -A 20 "tailwind"

echo
echo "=== Search all .css files in src ==="
find src -name "*.css" -type f

echo
echo "=== Check for next.config with tailwind setup ==="
find . -maxdepth 1 -name "next.config*" -o -name "tailwind.config*" | head -5

Repository: gamaware/earth-events-dashboard

Length of output: 1001


🏁 Script executed:

#!/bin/bash
echo "=== Check postcss.config.mjs ==="
cat postcss.config.mjs

echo
echo "=== Check next.config.ts ==="
cat next.config.ts

echo
echo "=== Check `@base-ui` package in node_modules or lock file ==="
if [ -f "package-lock.json" ]; then
  echo "package-lock.json found"
  grep -A 5 "@base-ui/react" package-lock.json | head -20
elif [ -f "pnpm-lock.yaml" ]; then
  echo "pnpm-lock.yaml found"
  grep -A 5 "@base-ui/react" pnpm-lock.yaml | head -20
elif [ -f "yarn.lock" ]; then
  echo "yarn.lock found"
  grep -A 5 "@base-ui/react" yarn.lock | head -20
fi

Repository: gamaware/earth-events-dashboard

Length of output: 1039


🏁 Script executed:

#!/bin/bash
# Search for any documentation or comments about data attributes in the codebase
echo "=== Search for how data attributes should be set or handled ==="
rg -n "data-orientation|data-horizontal|data-vertical" --type tsx --type ts -B 2 -A 2

echo
echo "=== Check if there are any TypeScript definitions or documentation ==="
find . -name "*.d.ts" | xargs grep -l "data-" 2>/dev/null || echo "No .d.ts files found with data-"

echo
echo "=== Look for any explicit setting of these data attributes in other components ==="
rg -n "data-horizontal\s*=" --type tsx --type ts
</script>

Repository: gamaware/earth-events-dashboard

Length of output: 504


🏁 Script executed:

#!/bin/bash
echo "=== Search for where data-horizontal or data-vertical are explicitly set ==="
rg "data-horizontal|data-vertical" | grep -v "data-\[" | grep -v "group-data"

echo
echo "=== Check `@base-ui` documentation or check if component automatically emits these ==="
rg "orientation" src/components/ui/toggle-group.tsx -B 2 -A 2

echo
echo "=== Look for any tests that might show expected behavior ==="
find . -name "*.test.*" -o -name "*.spec.*" | head -5

Repository: gamaware/earth-events-dashboard

Length of output: 1482


Update CSS selectors to match emitted data-orientation attribute.

The component emits data-orientation={orientation} but Tailwind selectors target non-existent data-vertical / data-horizontal attributes. Orientation-specific styles (flex-col layout, border positioning) will never apply.

Use arbitrary data attribute selectors instead:

Required changes

Line 45:

- data-vertical:flex-col data-vertical:items-stretch
+ data-[orientation=vertical]:flex-col data-[orientation=vertical]:items-stretch

Line 75:

- group-data-horizontal/toggle-group:...
- group-data-vertical/toggle-group:...
+ group-data-[orientation=horizontal]/toggle-group:...
+ group-data-[orientation=vertical]/toggle-group:...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/toggle-group.tsx` around lines 45 - 46, The Tailwind
selectors use non-existent attributes `data-vertical`/`data-horizontal` so
orientation-specific styles never apply; update the selectors in the ToggleGroup
component (the class strings that currently include
`data-vertical:`/`data-horizontal:`) to use arbitrary data-attribute selectors
that match the emitted prop `data-orientation={orientation}`, e.g. replace
`data-vertical:` with `data-[orientation=vertical]:` and `data-horizontal:` with
`data-[orientation=horizontal]:` for all orientation-dependent rules (the class
string around "group/toggle-group ..." and the other orientation-specific class
string later in the file).

Comment thread src/lib/api/types.ts
Comment on lines +22 to +28
export interface EONETGeometry {
magnitudeValue: number | null;
magnitudeUnit: string | null;
date: string;
type: "Point" | "Polygon";
coordinates: number[] | number[][];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd src/lib/api/types.ts

Repository: gamaware/earth-events-dashboard

Length of output: 57


🏁 Script executed:

cat -n src/lib/api/types.ts | head -100

Repository: gamaware/earth-events-dashboard

Length of output: 2521


🏁 Script executed:

rg -A 3 -B 3 "EONETGeometry|EONETGeoJSONFeature" --type ts --type tsx

Repository: gamaware/earth-events-dashboard

Length of output: 102


🏁 Script executed:

rg -A 3 -B 3 "EONETGeometry|EONETGeoJSONFeature" --type ts

Repository: gamaware/earth-events-dashboard

Length of output: 1919


🏁 Script executed:

rg -A 5 "formatCoordinates|coordinates" src/lib/utils/format.ts src/components/ --type ts

Repository: gamaware/earth-events-dashboard

Length of output: 3587


🏁 Script executed:

cat -n src/components/map/storm-tracks.tsx | head -50

Repository: gamaware/earth-events-dashboard

Length of output: 1774


🏁 Script executed:

cat -n src/components/map/event-popup.tsx | head -50

Repository: gamaware/earth-events-dashboard

Length of output: 2162


🏁 Script executed:

cat -n src/lib/utils/format.ts | grep -A 10 "formatCoordinates"

Repository: gamaware/earth-events-dashboard

Length of output: 521


🌐 Web query:

GeoJSON Point vs Polygon coordinates specification RFC 7946

💡 Result:

According to RFC 7946 (The GeoJSON Format), the coordinate specifications for Point and Polygon geometries differ in structure: Point: - The "coordinates" member is a single position array: [longitude, latitude] or optionally [longitude, latitude, altitude]. - Example: {"type": "Point", "coordinates": [100.0, 0.0]} Polygon: - The "coordinates" member is an array of linear ring coordinate arrays. - A linear ring is a closed LineString with 4 or more positions, where the first and last positions are equivalent. - The first ring is the exterior ring (counterclockwise per right-hand rule), subsequent rings are interior holes (clockwise). - Example (no holes): {"type": "Polygon", "coordinates": [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]]} Key differences: - Point: single 2D/3D position array (1 level of arrays). - Polygon: array of arrays of position arrays (3 levels: Polygon -> rings -> positions), with rings closed and winding-ordered.

Citations:


Use discriminated union types for Point and Polygon coordinates to match GeoJSON specification.

The current loose union type number[] | number[][] violates RFC 7946 (GeoJSON Format) where Point coordinates are a single position [number, number] and Polygon coordinates are an array of rings [number, number][][]. This loose typing forces multiple unsafe casts throughout the codebase (found in storm-tracks.tsx, event-popup.tsx, event-markers.tsx, and event-timeline.tsx). Refactor to a discriminated union that properly types each geometry:

  • Point: coordinates: [number, number]
  • Polygon: coordinates: [number, number][][]

This improves type safety and eliminates required casts per the coding guidelines requiring proper TypeScript typing in src/lib/**.

Also applies to: lines 77-80

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/api/types.ts` around lines 22 - 28, The EONETGeometry interface
currently uses a loose coordinates union (number[] | number[][]); change it to a
discriminated union so Point has coordinates: [number, number] and Polygon has
coordinates: [number, number][][] by replacing EONETGeometry with two variants
discriminated by the existing type field ("Point" | "Polygon"); update any other
geometry declarations (the similar block at lines referenced around 77-80) and
then update callers (e.g., storm-tracks.tsx, event-popup.tsx, event-markers.tsx,
event-timeline.tsx) to narrow on geometry.type before accessing coordinates to
remove unsafe casts and restore proper TypeScript checking.

gamaware added 2 commits April 3, 2026 23:58
- Replace mapbox-gl with maplibre-gl (free, open-source, no API key)
- Use CartoDB dark-matter tiles (no signup or credit card needed)
- Fix category toggle logic when all categories are active
- Fix cluster label font to Open Sans (compatible with CartoDB tiles)
- Remove globe projection (MapLibre uses flat map with dark tiles)
- Update CLAUDE.md and README.md to reflect MapLibre usage
Matches canonical pattern from professional-profile-site. The PRE_COMMIT_PAT
secret is intentionally used without a dedicated environment since the
workflow only creates PRs with hook version updates.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CLAUDE.md`:
- Around line 125-131: Update the documentation text that currently reads "Map
initialized with globe projection and dark style" to reflect the new flat map
behavior described in the PR; locate that string in CLAUDE.md and change it to
something like "Map initialized with flat projection and dark style" (or
otherwise remove any mention of globe projection) so the doc matches the code
change that removes globe projection.

In `@src/app/globals.css`:
- Around line 147-160: The Stylelint rule requires lowercase keywords—update the
`@keyframes` pulse-marker animation values that use currentColor to lowercase
currentcolor; locate the box-shadow properties inside `@keyframes` pulse-marker
(the 0%, 70%, and 100% blocks) and replace each occurrence of "currentColor"
with "currentcolor" to satisfy value-keyword-case.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 44a5ed82-c93c-464a-a00f-d4e879532b5b

📥 Commits

Reviewing files that changed from the base of the PR and between 693426b and 23fa0eb.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • CLAUDE.md
  • README.md
  • package.json
  • src/app/globals.css
  • src/components/layout/dashboard-shell.tsx
  • src/components/map/event-markers.tsx
  • src/components/map/event-popup.tsx
  • src/components/map/map-container.tsx
  • src/components/map/map-provider.tsx
  • src/components/map/storm-tracks.tsx
  • src/lib/constants/map-config.ts
  • zizmor.yml
✅ Files skipped from review due to trivial changes (3)
  • src/lib/constants/map-config.ts
  • src/components/map/storm-tracks.tsx
  • README.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • zizmor.yml
  • src/components/map/map-provider.tsx
  • src/components/layout/dashboard-shell.tsx
  • src/components/map/event-popup.tsx

Comment thread CLAUDE.md
Comment on lines +125 to +131
## MapLibre GL

- No API token needed (free, open-source)
- Map initialized with globe projection and dark style
- Use raw `maplibre-gl` (not react-map-gl) for full API access
- Map instance shared via React context (MapProvider)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Documentation inconsistency: globe projection was removed.

Line 128 states "Map initialized with globe projection and dark style", but the commit message in this PR explicitly states: "removes globe projection (flat map)". The map now uses a flat projection, not a globe projection.

📝 Proposed fix to align documentation with code
 ## MapLibre GL
 
 - No API token needed (free, open-source)
-- Map initialized with globe projection and dark style
+- Map initialized with flat projection and CartoDB dark-matter style
 - Use raw `maplibre-gl` (not react-map-gl) for full API access
 - Map instance shared via React context (MapProvider)
📝 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
## MapLibre GL
- No API token needed (free, open-source)
- Map initialized with globe projection and dark style
- Use raw `maplibre-gl` (not react-map-gl) for full API access
- Map instance shared via React context (MapProvider)
## MapLibre GL
- No API token needed (free, open-source)
- Map initialized with flat projection and CartoDB dark-matter style
- Use raw `maplibre-gl` (not react-map-gl) for full API access
- Map instance shared via React context (MapProvider)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` around lines 125 - 131, Update the documentation text that
currently reads "Map initialized with globe projection and dark style" to
reflect the new flat map behavior described in the PR; locate that string in
CLAUDE.md and change it to something like "Map initialized with flat projection
and dark style" (or otherwise remove any mention of globe projection) so the doc
matches the code change that removes globe projection.

Comment thread src/app/globals.css
Comment on lines +147 to +160
@keyframes pulse-marker {
0% {
box-shadow: 0 0 0 0 currentColor;
opacity: 1;
}
70% {
box-shadow: 0 0 0 10px currentColor;
opacity: 0;
}
100% {
box-shadow: 0 0 0 0 currentColor;
opacity: 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.

⚠️ Potential issue | 🟡 Minor

Fix casing: currentColorcurrentcolor per Stylelint rule.

The CSS spec accepts both, but Stylelint's value-keyword-case rule enforces lowercase for consistency.

🔧 Proposed fix
 `@keyframes` pulse-marker {
   0% {
-    box-shadow: 0 0 0 0 currentColor;
+    box-shadow: 0 0 0 0 currentcolor;
     opacity: 1;
   }
   70% {
-    box-shadow: 0 0 0 10px currentColor;
+    box-shadow: 0 0 0 10px currentcolor;
     opacity: 0;
   }
   100% {
-    box-shadow: 0 0 0 0 currentColor;
+    box-shadow: 0 0 0 0 currentcolor;
     opacity: 0;
   }
 }
📝 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
@keyframes pulse-marker {
0% {
box-shadow: 0 0 0 0 currentColor;
opacity: 1;
}
70% {
box-shadow: 0 0 0 10px currentColor;
opacity: 0;
}
100% {
box-shadow: 0 0 0 0 currentColor;
opacity: 0;
}
}
`@keyframes` pulse-marker {
0% {
box-shadow: 0 0 0 0 currentcolor;
opacity: 1;
}
70% {
box-shadow: 0 0 0 10px currentcolor;
opacity: 0;
}
100% {
box-shadow: 0 0 0 0 currentcolor;
opacity: 0;
}
}
🧰 Tools
🪛 Stylelint (17.6.0)

[error] 149-149: Expected "currentColor" to be "currentcolor" (value-keyword-case)

(value-keyword-case)


[error] 153-153: Expected "currentColor" to be "currentcolor" (value-keyword-case)

(value-keyword-case)


[error] 157-157: Expected "currentColor" to be "currentcolor" (value-keyword-case)

(value-keyword-case)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/globals.css` around lines 147 - 160, The Stylelint rule requires
lowercase keywords—update the `@keyframes` pulse-marker animation values that use
currentColor to lowercase currentcolor; locate the box-shadow properties inside
`@keyframes` pulse-marker (the 0%, 70%, and 100% blocks) and replace each
occurrence of "currentColor" with "currentcolor" to satisfy value-keyword-case.

@gamaware

gamaware commented Apr 4, 2026

Copy link
Copy Markdown
Owner Author

Code review

Found 3 issues:

  1. CONTRIBUTING.md references a Mapbox token that no longer exists (CLAUDE.md says "No API token needed (free, open-source)")

Step 4 of the Getting Started section instructs contributors to create a .env.local with NEXT_PUBLIC_MAPBOX_TOKEN=pk.xxx, but the PR migrated to MapLibre GL which requires no token. This will cause confusion for new contributors.

3. Run `pre-commit install && pre-commit install --hook-type commit-msg`
4. Create a `.env.local` file with your Mapbox token: `NEXT_PUBLIC_MAPBOX_TOKEN=pk.xxx`
5. Run `npm run dev`

  1. ADR-002 and ADR-001 reference stale library/version (CLAUDE.md says "Use raw maplibre-gl (not react-map-gl)")

ADR-002 is titled "Raw mapbox-gl over react-map-gl" and all content references mapbox-gl, but the actual dependency is maplibre-gl. ADR-001 says "Next.js 15" but package.json has 16.2.2. The .coderabbit.yaml explicitly requires documentation to match repository state.

# ADR-002: Raw mapbox-gl over react-map-gl
## Status
Accepted
## Context
Need to render an interactive globe with GeoJSON events, clustering,
category-specific markers, storm track LineStrings, and WMTS satellite layers.

Use Next.js 15 with App Router.
## Rationale

  1. days=0 forwarded to EONET API when "All" time range is selected (bug due to date-range-filter.tsx using value: 0 for "All")

When the user selects "All" in the date range filter, days=0 is forwarded to the NASA EONET API. The API does not accept days=0 as "all time" -- the days parameter should be omitted entirely. This means the "All" filter option likely returns no events or errors.

{ value: 90, label: "90d" },
{ value: 365, label: "1y" },
{ value: 0, label: "All" },
] as const;

if (params.status !== undefined) searchParams.set("status", params.status);
if (params.days !== undefined) searchParams.set("days", String(params.days));
if (params.source !== undefined) searchParams.set("source", params.source);

- Remove stale Mapbox token instruction from CONTRIBUTING.md
- Update ADR-001 to reference Next.js 16 and MapLibre
- Update ADR-002 to reflect maplibre-gl migration
- Fix days=0 API bug: omit days param when "All" is selected
- Fix stale Mapbox references in code comments

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

🧹 Nitpick comments (1)
src/components/layout/dashboard-shell.tsx (1)

49-50: Unsafe double type assertion bypasses TypeScript's type safety.

Using as unknown as GeoJSON.FeatureCollection disables compile-time verification that geojsonData conforms to the expected type. If the API response structure changes or contains unexpected data, runtime errors may occur without TypeScript warnings.

Consider typing the response properly in useGeoJSON to return GeoJSON.FeatureCollection | undefined, or add a runtime type guard.

♻️ Suggested approach

Option 1: Update useGeoJSON to return the correct type:

// In src/lib/hooks/use-geojson.ts
export function useGeoJSON(params?: UseGeoJSONParams): {
  data: GeoJSON.FeatureCollection | undefined;
  // ...
}

Option 2: Add a type guard in dashboard-shell:

- const geojsonFeatureCollection =
-   (geojsonData as unknown as GeoJSON.FeatureCollection) ?? null;
+ const geojsonFeatureCollection: GeoJSON.FeatureCollection | null =
+   geojsonData && 'type' in geojsonData && geojsonData.type === 'FeatureCollection'
+     ? geojsonData as GeoJSON.FeatureCollection
+     : null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/dashboard-shell.tsx` around lines 49 - 50, The current
double-cast (geojsonData as unknown as GeoJSON.FeatureCollection) bypasses
TypeScript safety; update the data flow to return a properly typed
FeatureCollection from useGeoJSON (change its return type to
GeoJSON.FeatureCollection | undefined in use-geojson.ts) or add a runtime type
guard in dashboard-shell that validates geojsonData is a FeatureCollection
before assigning to geojsonFeatureCollection (check properties like type ===
"FeatureCollection" and Array.isArray(features)); replace the unsafe assertion
with the typed return or the guarded assignment to ensure compile-time or
runtime safety for geojsonFeatureCollection.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/components/layout/dashboard-shell.tsx`:
- Around line 49-50: The current double-cast (geojsonData as unknown as
GeoJSON.FeatureCollection) bypasses TypeScript safety; update the data flow to
return a properly typed FeatureCollection from useGeoJSON (change its return
type to GeoJSON.FeatureCollection | undefined in use-geojson.ts) or add a
runtime type guard in dashboard-shell that validates geojsonData is a
FeatureCollection before assigning to geojsonFeatureCollection (check properties
like type === "FeatureCollection" and Array.isArray(features)); replace the
unsafe assertion with the typed return or the guarded assignment to ensure
compile-time or runtime safety for geojsonFeatureCollection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 904b6a8d-fbf5-4d90-a8aa-0b30ce553236

📥 Commits

Reviewing files that changed from the base of the PR and between 23fa0eb and 2511a48.

📒 Files selected for processing (7)
  • CONTRIBUTING.md
  • docs/adr/001-nextjs-over-spa.md
  • docs/adr/002-raw-mapbox-over-react-map-gl.md
  • src/components/layout/dashboard-shell.tsx
  • src/components/map/event-popup.tsx
  • src/lib/hooks/use-events.ts
  • src/lib/hooks/use-geojson.ts
✅ Files skipped from review due to trivial changes (3)
  • docs/adr/002-raw-mapbox-over-react-map-gl.md
  • docs/adr/001-nextjs-over-spa.md
  • CONTRIBUTING.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/lib/hooks/use-geojson.ts
  • src/lib/hooks/use-events.ts

@gamaware gamaware merged commit a7e9b01 into main Apr 4, 2026
15 checks passed
@gamaware gamaware deleted the feat/initial-dashboard branch April 4, 2026 06:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants