feat: initial earth events dashboard with NASA EONET API#5
Conversation
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)
📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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.
| hoverPopupRef.current = new mapboxgl.Popup({ | ||
| closeButton: false, | ||
| closeOnClick: false, | ||
| offset: 12, | ||
| className: "event-hover-popup", | ||
| }) | ||
| .setLngLat(coords) | ||
| .setHTML(buildPopupHTML(parseProperties(properties))) | ||
| .addTo(map); |
There was a problem hiding this comment.
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.
| map.on("mousemove", LAYER_UNCLUSTERED, handleMouseMove); | ||
| map.on("mouseleave", LAYER_UNCLUSTERED, handleMouseLeave); | ||
|
|
There was a problem hiding this comment.
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).
| const features = map.querySourceFeatures("events", { | ||
| filter: ["==", ["get", "id"], selectedEventId], | ||
| }); | ||
|
|
There was a problem hiding this comment.
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).
| useEffect(() => { | ||
| if (!map || !isLoaded) { | ||
| return; | ||
| } | ||
|
|
||
| const handleClusterClick = (e: MapMouseEvent) => { | ||
| const features = map.queryRenderedFeatures(e.point, { | ||
| layers: [LAYER_CLUSTERS], | ||
| }); | ||
| if (!features.length) { |
There was a problem hiding this comment.
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(...).
| "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, |
There was a problem hiding this comment.
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).
| 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 = |
There was a problem hiding this comment.
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.
| return ( | ||
| <ScrollArea className="h-full"> | ||
| <div className="flex flex-col gap-1.5"> |
There was a problem hiding this comment.
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).
| const events = useMemo(() => eventsData?.events ?? [], [eventsData]); | ||
| const geojsonFeatureCollection = | ||
| (geojsonData as unknown as GeoJSON.FeatureCollection) ?? null; | ||
|
|
There was a problem hiding this comment.
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.
| interface MapContextValue { | ||
| map: mapboxgl.Map | null; | ||
| setMap: (map: mapboxgl.Map) => void; | ||
| isLoaded: boolean; | ||
| setIsLoaded: (loaded: boolean) => void; | ||
| } |
There was a problem hiding this comment.
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.
| ## Decision | ||
|
|
||
| Use Next.js 15 with App Router. | ||
|
|
||
| ## Rationale |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 | 🟡 MinorHide 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.As per coding guidelines, `src/components/**` and `**/*.tsx` must be reviewed for accessibility (ARIA labels/semantic accessibility checks).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" /> ) }🤖 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 | 🟡 MinorAvoid inline
stylefor 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 | 🟡 MinorReplace inline color styles with class/token-based styling.
Inline
styleis 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 viaclassName.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 | 🟡 MinorClear map context during cleanup to avoid stale map references.
Unmount cleanup resets
isLoadedbut leaves the provider’smapvalue 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 | 🟡 MinorSet
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 | 🟡 MinorFix the “All” option ARIA label wording.
For the
Alloption, 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 | 🟡 MinorGuard
formatCoordinatesagainst non-finite values.
NaN/Infinitycurrently pass through and can produce malformed output likeNaN°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 | 🟡 MinorMissing 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 inDialogFooterlacks 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-labelensures 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 | 🟡 MinorUpdate action naming in structure docs to match the repo.
Line 28 says
update-pre-commit, but the action in this PR isupdate-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 | 🟡 MinorUpdate 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 | 🟡 MinorMake the total label follow the selected status.
This count is
events.length, but the dashboard supportsopen | closed | all. When the user switches away fromopen, the card still says "Active Events" even though the dataset can be closed or mixed. A neutral"Events"label or astatusprop 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 | 🟡 MinorPreserve 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.
As per coding guidelines, `src/lib/**`: "Review for proper TypeScript typing, error handling, and separation of concerns. API clients should handle errors gracefully."🛠️ 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>; }🤖 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 | 🟡 MinorPreserve 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.
As per coding guidelines, `src/lib/**`: "Review for proper TypeScript typing, error handling, and separation of concerns. API clients should handle errors gracefully."🛠️ 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>; }🤖 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 | 🟡 MinorUse
npx --no-install prettierto respect repo dependency scope.The repo has
.prettierrcbut prettier is not inpackage.jsondependencies. Running plainnpx prettiercan resolve to a globally installed version or trigger installation outside the repo's pinned setup. Usenpx --no-install prettierto 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 | 🟡 MinorActive-source badges lack keyboard accessibility.
The
Badgeelements 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 addrole="button",tabIndex={0}, andonKeyDownhandlers.♿ 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"> - × - </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"> + × + </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 | 🟡 MinorRemove unused
categoriesprop from interface and component.The
categoriesprop is declared inSidebarProps(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 indashboard-shell.tsx.Changes needed
- In
src/components/layout/sidebar.tsx: Removecategories: string[]from theSidebarPropsinterface- In
src/components/layout/dashboard-shell.tsx: Removecategories={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 | 🟡 MinorExpose 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 | 🟡 MinorThe 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 | 🟡 MinorStylelint will fail on
currentColorkeyword casing.Lines 149, 153, and 157 use
currentColor; your configured rule expects lowercasecurrentcolor.💡 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 | 🟡 MinorUse
data-selectedinstead ofdata-checkedfor item selection state.The check icon visibility is bound to
group-data-[checked=true], but cmdk usesdata-selectedto indicate selection (focus) state on Command.Item. Change togroup-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 | 🟡 MinorClick-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...propsis spread afteronClick, caller-providedonClickreplaces 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.
CardTitlerenders as a<div>, which lacks semantic meaning for assistive technologies. Screen reader users rely on heading elements for navigation. Consider accepting anasprop 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,
**/*.tsxfiles 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 becausetrivy-results.sarifwon't exist. You may want to add a file existence check or useif: 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 ofuseMap()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: StrengthenCATEGORY_IDStyping to preserve category key safety.
Object.keyswidens tostring[]; 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 addingaria-hiddento decorative icon.The
XIconis decorative since the adjacentsr-onlyspan provides the accessible name. Addingaria-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.xand192.168.xprivate networks but omits the172.16.0.0/12range (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 thesrc/-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
metadataexport, 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 unusedcategoriesprop as in Sidebar.The
categories: string[]prop (line 15) is declared inMobileDrawerPropsbut not destructured or used. Consider removing it for consistency with the fix insidebar.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
indexas 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-hiddenon body may affect accessibility in edge cases.While intentional for a full-screen dashboard layout,
overflow-hiddenon<body>could prevent scrolling if content overflows unexpectedly (e.g., high zoom levels or dynamic content). Ensure internal scroll containers (likeScrollArea) 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.FeatureCollectiondouble-cast completely bypasses type safety. IfgeojsonDatahas an unexpected structure, runtime errors could occur.Consider defining a proper return type for
useGeoJSONor adding runtime validation.♻️ Suggested approach
Either update
useGeoJSONto 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, andzizmoruselanguage: system, requiring these tools to be installed on each contributor's machine. Consider documenting these requirements in aCONTRIBUTING.mdor 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 moduleserver-onlyto 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 intogetCategoryConfig()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
📒 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.ymlAGENTS.mdCLAUDE.mdCODEOWNERSCONTRIBUTING.mdLICENSEREADME.mdSECURITY.mddocs/adr/001-nextjs-over-spa.mddocs/adr/002-raw-mapbox-over-react-map-gl.mddocs/adr/003-url-params-for-filter-state.mdeslint.config.mjssrc/__tests__/lib/format.test.tssrc/app/api/eonet/categories/route.tssrc/app/api/eonet/events/route.tssrc/app/api/eonet/geojson/route.tssrc/app/api/eonet/sources/route.tssrc/app/globals.csssrc/app/layout.tsxsrc/app/page.tsxsrc/components/events/event-card.tsxsrc/components/events/event-detail-panel.tsxsrc/components/events/event-list.tsxsrc/components/events/event-timeline.tsxsrc/components/filters/category-filter.tsxsrc/components/filters/date-range-filter.tsxsrc/components/filters/source-filter.tsxsrc/components/filters/status-filter.tsxsrc/components/layout/dashboard-shell.tsxsrc/components/layout/header.tsxsrc/components/layout/mobile-drawer.tsxsrc/components/layout/sidebar.tsxsrc/components/map/event-markers.tsxsrc/components/map/event-popup.tsxsrc/components/map/map-container.tsxsrc/components/map/map-controls.tsxsrc/components/map/map-provider.tsxsrc/components/map/storm-tracks.tsxsrc/components/stats/stat-card.tsxsrc/components/stats/stats-bar.tsxsrc/components/ui/badge.tsxsrc/components/ui/card.tsxsrc/components/ui/command.tsxsrc/components/ui/dialog.tsxsrc/components/ui/input-group.tsxsrc/components/ui/input.tsxsrc/components/ui/scroll-area.tsxsrc/components/ui/separator.tsxsrc/components/ui/sheet.tsxsrc/components/ui/skeleton.tsxsrc/components/ui/textarea.tsxsrc/components/ui/toggle-group.tsxsrc/components/ui/toggle.tsxsrc/lib/api/eonet-client.tssrc/lib/api/types.tssrc/lib/constants/categories.tssrc/lib/constants/map-config.tssrc/lib/hooks/use-categories.tssrc/lib/hooks/use-events.tssrc/lib/hooks/use-filters.tssrc/lib/hooks/use-geojson.tssrc/lib/utils/format.tsvitest.config.tszizmor.yml
💤 Files with no reviewable changes (1)
- AGENTS.md
| - uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 | ||
| with: | ||
| path: node_modules | ||
| key: node-modules-${{ hashFiles('package-lock.json') }} | ||
|
|
||
| - run: npx eslint . |
There was a problem hiding this comment.
🧩 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 -120Repository: gamaware/earth-events-dashboard
Length of output: 4013
🏁 Script executed:
# Check lines 48-53 specifically
sed -n '48,53p' .github/workflows/ci.yml | cat -nRepository: 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 -nRepository: 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:
- 1: https://docs.github.com/en/actions/reference/dependency-caching-reference
- 2: https://github.com/actions/cache
- 3: https://github.com/actions/cache/tree/v4
- 4: https://github.com/actions/cache/blob/main/caching-strategies.md
- 5: Restore original behavior of
cache-hitoutput actions/cache#1467 - 6: https://git.d.aiengines.ir/actions/cache/src/tag/v5.0.2/restore
- 7: https://github.com/actions/cache/
- 8: https://raw.githubusercontent.com/actions/cache/main/README.md
- 9: https://linkedin.com/company/devlexus
- 10: https://github.com/actions/cache/tree/main/restore
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 ciApply 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.
| - 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.
| - 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 |
There was a problem hiding this comment.
🧩 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_searchRepository: 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:
- 1: https://github.com/zizmorcore/zizmor
- 2: https://zizmor.sh/
- 3: https://github.com/zizmorcore/zizmor/releases
- 4: https://github.com/zizmorcore/zizmor/releases/tag/v1.23.0
- 5: https://docs.zizmor.sh/installation/
- 6: https://github.com/zizmorcore/zizmor/actions/runs/19788516542
🌐 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:
- 1: https://github.com/zizmorcore/zizmor-action
- 2: https://zizmor.sh/
- 3: https://github.com/marketplace/actions/zizmor-action
- 4: https://github.com/zizmorcore/zizmor
🌐 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:
- 1: https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions?learn=getting_started&learnProduct=actions
- 2: https://docs.github.com/en/code-security/tutorials/implement-supply-chain-best-practices/securing-builds?learn=end_to_end_supply_chain&learnProduct=code-security
- 3: https://github.blog/security/supply-chain-security/securing-the-open-source-supply-chain-across-github/
- 4: https://www.stepsecurity.io/blog/github-actions-security-best-practices
- 5: https://docs.github.com/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions
- 6: https://github.com/step-security/action-install-gh-release
- 7: https://docs.github.com/en/code-security/supply-chain-security/end-to-end-supply-chain/securing-builds
- 8: https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/use-artifact-attestations#verifying-an-artifact-attestation-for-binaries
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.
| 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 }} |
There was a problem hiding this comment.
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: falseAs 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.
| 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.
| "pattern": [ | ||
| "node_modules/.*", | ||
| ".next/.*", | ||
| "package-lock.json" |
There was a problem hiding this comment.
🧩 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))}")
PYRepository: 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}")
PYRepository: 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}")
PYRepository: 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/".
| 1. Do **not** open a public issue | ||
| 2. Email the maintainer with details of the vulnerability | ||
| 3. Include steps to reproduce if possible |
There was a problem hiding this comment.
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 possibleOption 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 possibleReplace 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.
| 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.
| <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> |
There was a problem hiding this comment.
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.
| 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} | ||
| /> |
There was a problem hiding this comment.
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.
| 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.
| 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 } |
There was a problem hiding this comment.
🧩 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=' srcRepository: 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.tsxRepository: 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 -20Repository: 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.
| 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.
| "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 |
There was a problem hiding this comment.
🧩 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.tsxRepository: 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.cssRepository: 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 -20Repository: 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.tsxRepository: gamaware/earth-events-dashboard
Length of output: 287
🏁 Script executed:
#!/bin/bash
echo "=== Full separator.tsx ==="
cat -n src/components/ui/separator.tsxRepository: 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 fRepository: 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 -5Repository: 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
fiRepository: 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 -5Repository: 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-stretchLine 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).
| export interface EONETGeometry { | ||
| magnitudeValue: number | null; | ||
| magnitudeUnit: string | null; | ||
| date: string; | ||
| type: "Point" | "Polygon"; | ||
| coordinates: number[] | number[][]; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd src/lib/api/types.tsRepository: gamaware/earth-events-dashboard
Length of output: 57
🏁 Script executed:
cat -n src/lib/api/types.ts | head -100Repository: gamaware/earth-events-dashboard
Length of output: 2521
🏁 Script executed:
rg -A 3 -B 3 "EONETGeometry|EONETGeoJSONFeature" --type ts --type tsxRepository: gamaware/earth-events-dashboard
Length of output: 102
🏁 Script executed:
rg -A 3 -B 3 "EONETGeometry|EONETGeoJSONFeature" --type tsRepository: gamaware/earth-events-dashboard
Length of output: 1919
🏁 Script executed:
rg -A 5 "formatCoordinates|coordinates" src/lib/utils/format.ts src/components/ --type tsRepository: gamaware/earth-events-dashboard
Length of output: 3587
🏁 Script executed:
cat -n src/components/map/storm-tracks.tsx | head -50Repository: gamaware/earth-events-dashboard
Length of output: 1774
🏁 Script executed:
cat -n src/components/map/event-popup.tsx | head -50Repository: 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:
- 1: https://datatracker.ietf.org/doc/html/rfc7946
- 2: https://rfc-editor.org/rfc/rfc7946.html
- 3: https://rfc-editor.org/rfc/rfc7946.txt
- 4: https://andrew-scott.co.uk/docs/rfc-txt/rfc7946.txt
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.
- 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.
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (12)
CLAUDE.mdREADME.mdpackage.jsonsrc/app/globals.csssrc/components/layout/dashboard-shell.tsxsrc/components/map/event-markers.tsxsrc/components/map/event-popup.tsxsrc/components/map/map-container.tsxsrc/components/map/map-provider.tsxsrc/components/map/storm-tracks.tsxsrc/lib/constants/map-config.tszizmor.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
| ## 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) | ||
|
|
There was a problem hiding this comment.
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.
| ## 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.
| @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; | ||
| } | ||
| } |
There was a problem hiding this comment.
Fix casing: currentColor → currentcolor 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.
| @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.
Code reviewFound 3 issues:
Step 4 of the Getting Started section instructs contributors to create a earth-events-dashboard/CONTRIBUTING.md Lines 7 to 9 in 23fa0eb
ADR-002 is titled "Raw mapbox-gl over react-map-gl" and all content references earth-events-dashboard/docs/adr/001-nextjs-over-spa.md Lines 14 to 16 in 23fa0eb
When the user selects "All" in the date range filter, earth-events-dashboard/src/components/filters/date-range-filter.tsx Lines 16 to 20 in 23fa0eb earth-events-dashboard/src/lib/hooks/use-events.ts Lines 28 to 30 in 23fa0eb |
- 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
There was a problem hiding this comment.
🧹 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.FeatureCollectiondisables compile-time verification thatgeojsonDataconforms 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
useGeoJSONto returnGeoJSON.FeatureCollection | undefined, or add a runtime type guard.♻️ Suggested approach
Option 1: Update
useGeoJSONto 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
📒 Files selected for processing (7)
CONTRIBUTING.mddocs/adr/001-nextjs-over-spa.mddocs/adr/002-raw-mapbox-over-react-map-gl.mdsrc/components/layout/dashboard-shell.tsxsrc/components/map/event-popup.tsxsrc/lib/hooks/use-events.tssrc/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
Summary
Tech Stack
Test plan
.env.localand runnpm run devnpm run buildwith zero errorsnpx vitest runwith 28/28 tests passingSummary by CodeRabbit
New Features
Documentation
Chores
Tests