Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
DATABASE_URL="postgresql://user:password@host:port/db"
WINDY_API_KEY="YOUR_WINDY_API_KEY"
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Do not quote placeholder values; keep style consistent with dotenv-linter.

Unquote to satisfy the linter and match common dotenv style.

-WINDY_API_KEY="YOUR_WINDY_API_KEY"
+WINDY_API_KEY=YOUR_WINDY_API_KEY
📝 Committable suggestion

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

Suggested change
WINDY_API_KEY="YOUR_WINDY_API_KEY"
WINDY_API_KEY=YOUR_WINDY_API_KEY
🧰 Tools
🪛 dotenv-linter (3.3.0)

[warning] 2-2: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)

🤖 Prompt for AI Agents
In .env around line 2, the placeholder value for WINDY_API_KEY is quoted which
violates the dotenv-linter style; remove the surrounding double quotes so the
line reads with an unquoted placeholder (no extra spaces or trailing characters)
and save/commit the change to keep style consistent.

3 changes: 3 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
SMITHERY_PROFILE_ID="your_smithery_profile_id_here"
SMITHERY_API_KEY="your_smithery_api_key_here"

# Windy.com Webcams API Key
WINDY_API_KEY="your_windy_api_key_here"

Comment on lines +6 to +8
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Example value: drop quotes to appease dotenv-linter (optional).

Not required, but aligns with the linter and house style.

-# Windy.com Webcams API Key
-WINDY_API_KEY="your_windy_api_key_here"
+# Windy.com Webcams API Key
+WINDY_API_KEY=your_windy_api_key_here
📝 Committable suggestion

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

Suggested change
# Windy.com Webcams API Key
WINDY_API_KEY="your_windy_api_key_here"
# Windy.com Webcams API Key
WINDY_API_KEY=your_windy_api_key_here
🧰 Tools
🪛 dotenv-linter (3.3.0)

[warning] 7-7: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)

🤖 Prompt for AI Agents
In .env.local.example around lines 6 to 8, the example WINDY_API_KEY value is
quoted which can trigger dotenv-linter and doesn't match house style; update the
example by removing the surrounding double quotes so the line reads
WINDY_API_KEY=your_windy_api_key_here (no quotes) and ensure there is a newline
after the entry.

# NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN is already used by mapbox-map.tsx
# Ensure it's also in your .env.local file if you haven't set it up yet.
# NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN="your_mapbox_public_token_here"
Expand Down
8 changes: 3 additions & 5 deletions components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import { Button } from '@/components/ui/button'
import {
Search,
CircleUserRound,
Map,
CalendarDays,
TentTree
TentTree,
} from 'lucide-react'
Comment on lines +11 to 12
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Remove unused TentTree import.

It’s no longer referenced after introducing SensorToggle; drop it to avoid lints.

-import {
-  Search,
-  CircleUserRound,
-  CalendarDays,
-  TentTree,
-} from 'lucide-react'
+import {
+  Search,
+  CircleUserRound,
+  CalendarDays,
+} from 'lucide-react'
📝 Committable suggestion

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

Suggested change
TentTree,
} from 'lucide-react'
import {
Search,
CircleUserRound,
CalendarDays,
} from 'lucide-react'
🤖 Prompt for AI Agents
In components/header.tsx around lines 11 to 12, the TentTree import from
'lucide-react' is unused after introducing SensorToggle; remove TentTree from
the import list so only referenced icons remain to satisfy linters and keep
imports minimal.

import { MapToggle } from './map-toggle'
import { ProfileToggle } from './profile-toggle'
import { SensorToggle } from './sensor-toggle'

export const Header = () => {
return (
Expand Down Expand Up @@ -43,9 +43,7 @@ export const Header = () => {
<Search className="h-[1.2rem] w-[1.2rem]" />
</Button>

<Button variant="ghost" size="icon">
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
<SensorToggle />

<ModeToggle />

Expand Down
1 change: 1 addition & 0 deletions components/map-toggle-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum MapToggleEnum {
FreeMode,
RealTimeMode,
DrawingMode, // Added new mode for drawing and measurement
SensorMode,
}
Comment on lines +9 to 10
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Prefer string enum to avoid ordinal coupling; consider aligning name with “Webcams”.

Use string values to prevent future reordering bugs; also consider renaming SensorMode to WebcamMode for consistency with the feature and PR wording.

Apply:

-export enum MapToggleEnum {
-  FreeMode,
-  RealTimeMode,
-  DrawingMode, // Added new mode for drawing and measurement
-  SensorMode,
-}
+export enum MapToggleEnum {
+  FreeMode = 'FreeMode',
+  RealTimeMode = 'RealTimeMode',
+  DrawingMode = 'DrawingMode', // Added new mode for drawing and measurement
+  SensorMode = 'SensorMode', // consider: 'WebcamMode'
+}
📝 Committable suggestion

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

Suggested change
SensorMode,
}
export enum MapToggleEnum {
FreeMode = 'FreeMode',
RealTimeMode = 'RealTimeMode',
DrawingMode = 'DrawingMode', // Added new mode for drawing and measurement
SensorMode = 'SensorMode', // consider: 'WebcamMode'
}
🤖 Prompt for AI Agents
In components/map-toggle-context.tsx around lines 9-10, the enum currently uses
numeric/ordinal values and is named SensorMode; replace it with a string enum
named WebcamMode (e.g., export enum WebcamMode { On = "On", Off = "Off", ... })
to avoid ordinal coupling and align naming with the feature. Update all
imports/usages, type annotations, switch/case and default handling, plus any
serialization/deserialization, localStorage or API payloads to use the string
values and the new name, and update tests and snapshots accordingly.


interface MapToggleContextType {
Expand Down
7 changes: 3 additions & 4 deletions components/map-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ export function MapToggle() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="relative">
<Map className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
<span className="sr-only">Toggle map mode</span>
<Button variant="ghost" size="icon">
<Map className="h-[1.2rem] w-[1.2rem]" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
Expand All @@ -30,7 +29,7 @@ export function MapToggle() {
My Maps
</DropdownMenuItem>
<DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.DrawingMode)}}>
<Pencil className="h-[1rem] w-[1rem] mr-2" />
<Pencil className="h-[1rem] w-[1.2rem] mr-2" />
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Normalize icon sizing (nit).

For visual consistency with other icons, consider matching height to 1.2rem.

-          <Pencil className="h-[1rem] w-[1.2rem] mr-2" />
+          <Pencil className="h-[1.2rem] w-[1.2rem] mr-2" />
📝 Committable suggestion

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

Suggested change
<Pencil className="h-[1rem] w-[1.2rem] mr-2" />
<Pencil className="h-[1.2rem] w-[1.2rem] mr-2" />
🤖 Prompt for AI Agents
In components/map-toggle.tsx around line 32, the Pencil icon uses className
"h-[1rem] w-[1.2rem] mr-2" which makes its height inconsistent with other icons;
change the height to 1.2rem to match (e.g., update className to use h-[1.2rem]
w-[1.2rem] mr-2) so the icon dimensions are normalized with the rest of the UI.

Draw & Measure
</DropdownMenuItem>
</DropdownMenuContent>
Expand Down
58 changes: 57 additions & 1 deletion components/map/mapbox-map.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useEffect, useRef, useCallback } from 'react' // Removed useState
import { useEffect, useRef, useCallback, useState } from 'react'
import mapboxgl from 'mapbox-gl'
import MapboxDraw from '@mapbox/mapbox-gl-draw'
import * as turf from '@turf/turf'
Expand All @@ -11,6 +11,7 @@ import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'
import { useMapToggle, MapToggleEnum } from '../map-toggle-context'
import { useMapData } from './map-data-context'; // Add this import
import { useMapLoading } from '../map-loading-context'; // Import useMapLoading
import { getSensors, Sensor } from '@/lib/sensors';

mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN as string;

Expand All @@ -32,6 +33,9 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number
const { mapData, setMapData } = useMapData(); // Consume the new context, get setMapData
const { setIsMapLoaded } = useMapLoading(); // Get setIsMapLoaded from context
const previousMapTypeRef = useRef<MapToggleEnum | null>(null)
const [sensors, setSensors] = useState<Sensor[]>([]);
const sensorMarkersRef = useRef<{ [id: string]: mapboxgl.Marker }>({});


// Refs for long-press functionality
const longPressTimerRef = useRef<NodeJS.Timeout | null>(null);
Expand Down Expand Up @@ -505,6 +509,58 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number
}
}, [position, updateMapPosition, mapType])

useEffect(() => {
if (mapType === MapToggleEnum.SensorMode) {
const fetchSensors = async () => {
try {
const windyResponse = await getSensors();
setSensors(windyResponse.webcams);
} catch (error) {
console.error('Error fetching sensors:', error);
toast.error('Failed to fetch sensors.');
}
};
fetchSensors();
} else {
// Clear sensors and markers when not in SensorMode
setSensors([]);
Object.values(sensorMarkersRef.current).forEach(marker => marker.remove());
sensorMarkersRef.current = {};
}
}, [mapType]);

useEffect(() => {
if (map.current && mapType === MapToggleEnum.SensorMode) {
// Clear existing markers
Object.values(sensorMarkersRef.current).forEach(marker => marker.remove());
sensorMarkersRef.current = {};

sensors.forEach(sensor => {
const el = document.createElement('div');
el.className = 'sensor-marker';
el.style.width = '30px';
el.style.height = '30px';
el.style.borderRadius = '50%';
el.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
el.style.color = 'white';
el.style.display = 'flex';
el.style.justifyContent = 'center';
el.style.alignItems = 'center';
el.style.cursor = 'pointer';
el.innerHTML = '&#8599;';

Comment on lines +538 to +551
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Avoid innerHTML even for constants (nit).

Use textContent for the marker’s glyph to keep a consistent no-HTML-injection pattern.

-        el.innerHTML = '&#8599;';
+        el.textContent = '↗';
📝 Committable suggestion

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

Suggested change
sensors.forEach(sensor => {
const el = document.createElement('div');
el.className = 'sensor-marker';
el.style.width = '30px';
el.style.height = '30px';
el.style.borderRadius = '50%';
el.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
el.style.color = 'white';
el.style.display = 'flex';
el.style.justifyContent = 'center';
el.style.alignItems = 'center';
el.style.cursor = 'pointer';
el.innerHTML = '&#8599;';
sensors.forEach(sensor => {
const el = document.createElement('div');
el.className = 'sensor-marker';
el.style.width = '30px';
el.style.height = '30px';
el.style.borderRadius = '50%';
el.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
el.style.color = 'white';
el.style.display = 'flex';
el.style.justifyContent = 'center';
el.style.alignItems = 'center';
el.style.cursor = 'pointer';
el.textContent = '';
🤖 Prompt for AI Agents
In components/map/mapbox-map.tsx around lines 538 to 551, the marker glyph is
being assigned via el.innerHTML which allows HTML and contradicts the
no-HTML-injection pattern; replace that assignment with el.textContent (or
el.innerText) and use the literal Unicode character (e.g. ↗) or a plain string
so the marker glyph is set as text rather than HTML.

const marker = new mapboxgl.Marker(el)
.setLngLat([sensor.location.longitude, sensor.location.latitude])
.setPopup(new mapboxgl.Popup({ offset: 25 }).setHTML(
`<a href="${sensor.urls.webcam}" target="_blank" rel="noopener noreferrer"><h3>${sensor.title}</h3><img src="${sensor.images.current.preview}" alt="${sensor.title}" style="width:100%;" /></a>`
))
.addTo(map.current!);

sensorMarkersRef.current[sensor.id] = marker;
});
}
}, [sensors, mapType]);

// Effect to handle map updates from MapDataContext
useEffect(() => {
if (mapData.targetPosition && map.current) {
Expand Down
8 changes: 3 additions & 5 deletions components/mobile-icons-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import {
CircleUserRound,
Map,
CalendarDays,
TentTree,
Paperclip,
ArrowRight,
Plus
} from 'lucide-react'
import { History } from '@/components/history'
import { MapToggle } from './map-toggle'
import { ModeToggle } from './mode-toggle'
import { MapToggle } from './map-toggle'
import { SensorToggle } from './sensor-toggle'
Comment on lines +18 to +19
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Drop unused/undesirable Map icon import (shadows global).

The file imports Map from lucide-react (Line 10) but doesn’t use it; it also triggers the “shadowing global Map” lint. Remove it.

// Remove Map from this import list:
import {
  Search,
  CircleUserRound,
-  Map,
  CalendarDays,
  Paperclip,
  ArrowRight,
  Plus
} from 'lucide-react'
🤖 Prompt for AI Agents
In components/mobile-icons-bar.tsx around lines 18 to 19, the import list from
'lucide-react' includes the unused identifier Map which shadows the global Map
and triggers a lint error; remove Map from that import list (so the import only
includes the actually used icons: Search, CircleUserRound, CalendarDays,
Paperclip, ArrowRight, Plus), save the file and run the linter/format step to
ensure no remaining unused-import warnings.


export const MobileIconsBar: React.FC = () => {
const [, setMessages] = useUIState<typeof AI>()
Expand All @@ -42,9 +42,7 @@ export const MobileIconsBar: React.FC = () => {
<Button variant="ghost" size="icon">
<Search className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
<Button variant="ghost" size="icon">
<TentTree className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
<SensorToggle />
<Button variant="ghost" size="icon">
<Paperclip className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
Expand Down
32 changes: 32 additions & 0 deletions components/sensor-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client'

import * as React from 'react'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { TentTree, RadioTower } from 'lucide-react'
import { useMapToggle, MapToggleEnum } from './map-toggle-context'
Comment on lines +11 to +12
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Verify enum member exists (SensorMode/WebcamMode).

Ensure MapToggleEnum.SensorMode exists. If the intended name is WebcamMode, align the enum and all call sites.


🏁 Script executed:

#!/bin/bash
# Verify MapToggleEnum includes SensorMode or WebcamMode and find call sites.
rg -nP -C2 'enum\s+MapToggleEnum\b|MapToggleEnum\.(SensorMode|WebcamMode)' --type=ts --type=tsx

Length of output: 27


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== Searching for MapToggleEnum occurrences ==="
rg -n --hidden -S '\bMapToggleEnum\b' || true

echo
echo "=== Searching for enum definition 'enum MapToggleEnum' ==="
rg -n --hidden -S 'enum\s+MapToggleEnum\b' -C3 || true

echo
echo "=== Searching for files named map-toggle-context* ==="
rg -n --hidden -S 'map-toggle-context' || true

echo
echo "=== Attempt to show likely context file(s) (map-toggle-context.*) ==="
for f in $(rg -n --hidden -S --files-with-matches 'map-toggle-context' || true); do
  echo
  echo "---- File: $f ----"
  sed -n '1,240p' "$f" || true
done

Length of output: 16699


Add SensorMode to MapToggleEnum or update the call sites to the correct enum name

MapToggleEnum is currently: FreeMode, RealTimeMode, DrawingMode — but code references MapToggleEnum.SensorMode (or an intended WebcamMode), which will break typechecking/runtime. Either add the missing member to components/map-toggle-context.tsx or change each call site to an existing/intentional enum value.

Locations:

  • Add member: components/map-toggle-context.tsx (export enum MapToggleEnum)
  • Update usages: components/sensor-toggle.tsx:25; components/map/mapbox-map.tsx:513, components/map/mapbox-map.tsx:533
🤖 Prompt for AI Agents
In components/sensor-toggle.tsx around lines 11-12 (and usages at
components/sensor-toggle.tsx:25, components/map/mapbox-map.tsx:513 & 533), the
code references MapToggleEnum.SensorMode which does not exist on the exported
enum in components/map-toggle-context.tsx; add the missing enum member (e.g.,
SensorMode or WebcamMode) to the export enum MapToggleEnum in
components/map-toggle-context.tsx, or alternatively update each call site to use
an existing/intentional enum value; ensure the chosen enum name is used
consistently across import sites and update any type annotations so TypeScript
compiles.


export function SensorToggle() {
const { setMapType } = useMapToggle();

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
Comment on lines +20 to +22
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add an accessible name to the icon-only button.

Icon-only controls should expose an aria-label.

-        <Button variant="ghost" size="icon">
+        <Button variant="ghost" size="icon" aria-label="Webcams">
📝 Committable suggestion

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

Suggested change
<Button variant="ghost" size="icon">
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
<Button variant="ghost" size="icon" aria-label="Webcams">
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
🤖 Prompt for AI Agents
In components/sensor-toggle.tsx around lines 20 to 22, the Button is icon-only
and lacks an accessible name; add an accessible name by supplying an appropriate
aria-label (or aria-labelledby) to the Button (e.g., aria-label="Toggle sensor"
or a label that reflects the specific action/state), or include visually hidden
text inside the button to provide the accessible name; if this is a toggle, also
ensure to expose state via aria-pressed or aria-checked as appropriate.

</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.SensorMode)}}>
<RadioTower className="h-[1rem] w-[1.2rem] mr-2" />
Sensors
</DropdownMenuItem>
Comment on lines +25 to +28
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Align UI text with feature: “Webcams”, not “Sensors”.

PR integrates Windy Webcams; use “Webcams” for clarity. Optionally use a camera icon for the menu item.

-        <DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.SensorMode)}}>
-          <RadioTower className="h-[1rem] w-[1.2rem] mr-2" />
-          Sensors
+        <DropdownMenuItem onClick={() => { setMapType(MapToggleEnum.SensorMode) }}>
+          <RadioTower className="h-[1rem] w-[1.2rem] mr-2" />
+          Webcams

If you prefer the camera glyph:

  • Update imports: import { TentTree, RadioTower, Camera } from 'lucide-react'
  • Replace RadioTower with Camera.
📝 Committable suggestion

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

Suggested change
<DropdownMenuItem onClick={() => {setMapType(MapToggleEnum.SensorMode)}}>
<RadioTower className="h-[1rem] w-[1.2rem] mr-2" />
Sensors
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { setMapType(MapToggleEnum.SensorMode) }}>
<RadioTower className="h-[1rem] w-[1.2rem] mr-2" />
Webcams
</DropdownMenuItem>

</DropdownMenuContent>
</DropdownMenu>
)
}
56 changes: 56 additions & 0 deletions lib/sensors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export interface Sensor {
id: string;
title: string;
location: {
latitude: number;
longitude: number;
};
images: {
current: {
preview: string;
};
daylight: {
preview: string;
}
};
urls: {
webcam: string;
};
}
Comment on lines +1 to +19
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Naming: “Sensor” may confuse; prefer “Webcam” types.

Aligning names with the data source improves clarity across UI and API.

-export interface Sensor {
+export interface Webcam {
   id: string;
   title: string;
   location: {
     latitude: number;
     longitude: number;
   };
   images: {
     current: {
       preview: string;
     };
     daylight: {
       preview: string;
     }
   };
   urls: {
     webcam: string;
   };
 }
 
-export interface WindyResponse {
-  webcams: Sensor[];
+export interface WindyResponse {
+  webcams: Webcam[];
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In lib/sensors.ts lines 1-19, the exported interface is named Sensor but should
be renamed to Webcam to match the data source; change the interface declaration
and its export from Sensor to Webcam, update any JSDoc or inline comments
accordingly, and then update all project references/imports to use the new
Webcam type (search & replace imports and type annotations) to keep API and UI
naming consistent.


export interface WindyResponse {
webcams: Sensor[];
}
Comment on lines +21 to +23
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Confirm response shape matches Windy API.

Many APIs nest results; add runtime validation (e.g., zod) or adapt to the actual schema before returning.


🌐 Web query:

Windy Webcams API v3: What is the exact JSON shape for GET /webcams? Does it return { webcams: [...] } or a nested object?

💡 Result:

Short answer: GET /webcams (Webcams API v3) returns a top-level JSON array of webcam objects (WebcamV3Schema[]), not an object like { "webcams": [...] }. See the V3 docs/transfer notes. [1][2]

Sources:

  • Windy Webcams API docs (V3 overview). [1]
  • Migration / result schema notes showing V3 returns WebcamV3Schema[] (V2 used { result: { webcams: [...] } }). [2]

Windy v3 returns a top-level array — update types & add runtime validation

Windy Webcams API v3 returns a top-level array of webcam objects (WebcamV3Schema[]). lib/sensors.ts defines WindyResponse as { webcams: Sensor[] } which is incorrect.

  • Fix: replace/adapt to the actual shape (e.g., export type WindyResponse = Sensor[] or change the fetch/parsing to map the returned array into { webcams }).
  • Add runtime validation (zod or similar) to assert the array and element schema before use.

Location: lib/sensors.ts (lines 21–23)


export async function getSensors(): Promise<WindyResponse> {
const apiKey = process.env.WINDY_API_KEY;
if (!apiKey || apiKey === 'YOUR_WINDY_API_KEY') {
console.error('WINDY_API_KEY is not defined or not set in the environment variables.');
throw new Error('WINDY_API_KEY is not defined or not set in the environment variables.');
}
Comment on lines +27 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Unify placeholder value with .env samples.

Use the same placeholder everywhere to reduce confusion.

-  if (!apiKey || apiKey === 'YOUR_WINDY_API_KEY') {
+  if (!apiKey || apiKey === 'your_windy_api_key_here') {
📝 Committable suggestion

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

Suggested change
if (!apiKey || apiKey === 'YOUR_WINDY_API_KEY') {
console.error('WINDY_API_KEY is not defined or not set in the environment variables.');
throw new Error('WINDY_API_KEY is not defined or not set in the environment variables.');
}
if (!apiKey || apiKey === 'your_windy_api_key_here') {
console.error('WINDY_API_KEY is not defined or not set in the environment variables.');
throw new Error('WINDY_API_KEY is not defined or not set in the environment variables.');
}
🤖 Prompt for AI Agents
In lib/sensors.ts around lines 27 to 30, the code compares apiKey to the literal
'YOUR_WINDY_API_KEY' which differs from the placeholder used in our .env
samples; change that literal to exactly match the placeholder string used in the
repository's .env example (replace 'YOUR_WINDY_API_KEY' with the .env sample
placeholder) so the environment-check is consistent across code and docs, and
keep the error message text aligned with that same placeholder.


Comment on lines +25 to +31
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical: Secret usage in a client-importable module.

This module is imported by a client component (Mapbox). Accessing process.env.WINDY_API_KEY here will either leak the key (if bundled) or be undefined at runtime. Fetch secrets server-side only and expose a safe API route.

Apply server-only guard and move fetch to an API route; then have clients call the route:

+import 'server-only';

Example API route (add new file app/api/sensors/route.ts):

// app/api/sensors/route.ts
import 'server-only';

export const dynamic = 'force-dynamic';

export async function GET(req: Request) {
  const apiKey = process.env.WINDY_API_KEY;
  if (!apiKey) {
    return new Response(JSON.stringify({ error: 'Missing WINDY_API_KEY' }), { status: 500 });
  }

  const { searchParams } = new URL(req.url);
  const bbox = searchParams.get('bbox') ?? '';
  const url = new URL('https://api.windy.com/webcams/api/v3/webcams');
  url.searchParams.set('include', 'location,images,urls');
  if (bbox) url.searchParams.set('bbox', bbox);

  const ac = new AbortController();
  const to = setTimeout(() => ac.abort(), 10000);
  try {
    const res = await fetch(url.toString(), {
      headers: { 'x-windy-api-key': apiKey },
      signal: ac.signal,
      cache: 'no-store',
    });
    if (!res.ok) {
      const txt = await res.text();
      return new Response(JSON.stringify({ error: txt || res.statusText }), { status: res.status });
    }
    const json = await res.json();
    return new Response(JSON.stringify(json), { status: 200 });
  } catch (e) {
    const msg = (e as Error)?.message ?? 'unknown error';
    return new Response(JSON.stringify({ error: msg }), { status: 500 });
  } finally {
    clearTimeout(to);
  }
}

Then update the client to fetch /api/sensors (see mapbox-map.tsx comments).

try {
console.log('Fetching sensors with API key:', apiKey ? 'present' : 'missing');
const response = await fetch('https://api.windy.com/webcams/api/v3/webcams?include=location,images,urls', {
method: 'GET',
headers: {
'x-windy-api-key': apiKey,
},
});

console.log('Windy API response status:', response.status);
Comment on lines +33 to +41
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add timeout/abort and no-store caching to the upstream fetch.

Prevents hung requests and stale data; also safer for quota.

-    const response = await fetch('https://api.windy.com/webcams/api/v3/webcams?include=location,images,urls', {
-      method: 'GET',
-      headers: {
-        'x-windy-api-key': apiKey,
-      },
-    });
+    const ac = new AbortController();
+    const timeout = setTimeout(() => ac.abort(), 10000);
+    const response = await fetch(
+      'https://api.windy.com/webcams/api/v3/webcams?include=location,images,urls',
+      {
+        method: 'GET',
+        headers: { 'x-windy-api-key': apiKey },
+        signal: ac.signal,
+        cache: 'no-store',
+      }
+    );
+    clearTimeout(timeout);
📝 Committable suggestion

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

Suggested change
console.log('Fetching sensors with API key:', apiKey ? 'present' : 'missing');
const response = await fetch('https://api.windy.com/webcams/api/v3/webcams?include=location,images,urls', {
method: 'GET',
headers: {
'x-windy-api-key': apiKey,
},
});
console.log('Windy API response status:', response.status);
console.log('Fetching sensors with API key:', apiKey ? 'present' : 'missing');
const ac = new AbortController();
const timeout = setTimeout(() => ac.abort(), 10000);
const response = await fetch(
'https://api.windy.com/webcams/api/v3/webcams?include=location,images,urls',
{
method: 'GET',
headers: { 'x-windy-api-key': apiKey },
signal: ac.signal,
cache: 'no-store',
}
);
clearTimeout(timeout);
console.log('Windy API response status:', response.status);
🤖 Prompt for AI Agents
In lib/sensors.ts around lines 33 to 41, the upstream fetch lacks a
timeout/abort mechanism and cache control; wrap the request in an
AbortController with a setTimeout that calls controller.abort() after a
reasonable ms (e.g. 5-10s), pass controller.signal to fetch, clear the timeout
after fetch completes, and handle the abort exception pathway; also add fetch
option cache: 'no-store' to prevent caching of responses and ensure fresh
requests and safer quota usage.


if (!response.ok) {
const errorText = await response.text();
console.error('Failed to fetch sensors:', response.statusText, errorText);
throw new Error(`Failed to fetch sensors: ${response.statusText}`);
}

const data = await response.json();
console.log('Successfully fetched sensor data:', data);
return data;
Comment on lines +49 to +51
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick

Avoid logging entire payloads.

Large logs hurt perf and may expose data unnecessarily.

-    console.log('Successfully fetched sensor data:', data);
+    console.debug('Successfully fetched sensor data');
📝 Committable suggestion

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

Suggested change
const data = await response.json();
console.log('Successfully fetched sensor data:', data);
return data;
const data = await response.json();
console.debug('Successfully fetched sensor data');
return data;
🤖 Prompt for AI Agents
In lib/sensors.ts around lines 49 to 51, the code currently logs the entire
fetched payload (console.log('Successfully fetched sensor data:', data)); remove
or replace that full-payload log to avoid performance and data-exposure issues —
either drop the log entirely or log a small summary (e.g., number of records,
keys, or first N items) and/or use a conditional debug logger tied to an
environment flag; if sensitive fields exist, redact them before logging. Ensure
you do not stringify or print the whole `data` object in production code.

} catch (error) {
console.error('Error in getSensors:', error);
throw error;
}
}