Skip to content

feat(weather): redesigned weather app with TypeScript and web components#670

Draft
nicomiguelino wants to merge 2 commits intomasterfrom
redesign-weather-app
Draft

feat(weather): redesigned weather app with TypeScript and web components#670
nicomiguelino wants to merge 2 commits intomasterfrom
redesign-weather-app

Conversation

@nicomiguelino
Copy link
Contributor

@nicomiguelino nicomiguelino commented Feb 8, 2026

User description

Summary

Redesigned weather app using TypeScript, edge-apps-library utilities, and web components. Includes current weather display and 8-item hourly forecast with glassmorphic design matching Figma specifications.


PR Type

Enhancement, Documentation


Description

  • Add redesigned weather edge app UI

  • Fetch current + 8-item forecast

  • Reverse-geocode coordinates to city name

  • Add manifests, scripts, and README


Diagram Walkthrough

flowchart LR
  A["`main.ts` (bootstrap + refresh loop)"]
  B["`location.ts` (reverse geocode)"]
  C["`weather.ts` (current + forecast fetch)"]
  D["OpenWeatherMap APIs"]
  E["DOM (index.html + CSS)"]
  F["`settings.ts` (unit setting)"]
  G["`weather-icons.ts` (icon map)"]
  A -- "get location name" --> B
  A -- "get weather + forecast" --> C
  B -- "reverse geocode request" --> D
  C -- "weather/forecast requests" --> D
  C -- "resolve icon keys" --> G
  C -- "read unit" --> F
  A -- "render updates" --> E
Loading

File Walkthrough

Relevant files
Enhancement
6 files
location.ts
Reverse-geocode coordinates into displayed city name         
+37/-0   
main.ts
Initialize app, render weather, refresh periodically         
+120/-0 
weather-icons.ts
Map icon keys to bundled SVG assets                                           
+47/-0   
weather.ts
Fetch and format current weather and forecast                       
+179/-0 
style.css
Implement glassmorphic layout and responsive styles           
+234/-0 
index.html
Add app markup with header and forecast slots                       
+49/-0   
Configuration changes
3 files
settings.ts
Add unit setting helper with default                                         
+9/-0     
screenly.yml
Add manifest with API key and analytics settings                 
+52/-0   
screenly_qc.yml
Add QC manifest mirroring production settings                       
+52/-0   
Documentation
1 files
README.md
Document settings and local build commands                             
+31/-0   
Dependencies
1 files
package.json
Configure bun scripts, tooling, and workspace deps             
+35/-0   

… web components

- Modernized implementation using TypeScript, edge-apps-library utilities, and web components
- Includes current weather display and 8-item hourly forecast with glassmorphic design

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@github-actions
Copy link

github-actions bot commented Feb 8, 2026

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 No relevant tests
🔒 Security concerns

XSS injection:
renderForecastItems writes API-derived strings into the DOM via innerHTML. This could allow HTML/script injection if the upstream data is malicious or unexpectedly formatted. Consider using DOM APIs (textContent, setAttribute) or sanitizing content before insertion.

⚡ Recommended focus areas for review

Possible Issue

The OpenWeatherMap response validation treats main.temp as truthy, which will incorrectly reject valid temperatures of 0. Also, the code later rounds temp_max/temp_min without validating their presence, which can yield NaN and propagate to the UI.

// Validate OpenWeatherMap API response
// Note: `cod` can be number (200) or string ("200") depending on the endpoint
function isValidWeatherResponse(data: {
  cod?: number | string
  main?: { temp?: number }
}): boolean {
  return (data.cod === 200 || data.cod === '200') && Boolean(data.main?.temp)
}

function getIconSrc(weatherId: number, dt: number, tz: string): string {
  const iconKey = getWeatherIconKey(weatherId, dt, tz)
  return WEATHER_ICONS[iconKey] || WEATHER_ICONS['clear']
}

// Get current weather data
export async function getCurrentWeather(
  lat: number,
  lng: number,
  tz: string,
): Promise<CurrentWeatherData | null> {
  try {
    const apiKey = getSetting<string>('openweathermap_api_key')
    if (!apiKey) {
      return null
    }

    const unit = getMeasurementUnit()

    const response = await fetch(
      `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lng}&units=${unit}&appid=${apiKey}`,
    )

    if (!response.ok) {
      console.warn(
        'Failed to get weather data: OpenWeatherMap API responded with',
        response.status,
        response.statusText,
      )
      return null
    }

    const data = await response.json()

    if (!isValidWeatherResponse(data)) {
      return null
    }

    const temperature = Math.round(data.main.temp)
    const tempHigh = Math.round(data.main.temp_max)
    const tempLow = Math.round(data.main.temp_min)
    const weatherId = data.weather?.[0]?.id ?? null

    if (!weatherId) {
      return null
    }
XSS Risk

Forecast rendering uses innerHTML with values derived from API data (e.g., description used in alt, temperatures, labels). If any of those strings contain markup (or upstream is compromised), this can become DOM injection. Prefer creating elements and assigning textContent/setAttribute, or sanitize before insertion.

function renderForecastItems(items: ForecastItem[]) {
  if (!forecastItemsEl) return

  forecastItemsEl.innerHTML = items
    .map(
      (item) => `
      <div class="forecast-item">
        <span class="forecast-item-temp">${item.displayTemp}</span>
        <img class="forecast-item-icon" src="${item.iconSrc}" alt="${item.iconAlt}" />
        <span class="forecast-item-time">${item.timeLabel}</span>
      </div>
    `,
    )
    .join('')
}
UX Consistency

The first forecast item is labeled NOW, but the /forecast endpoint returns 3-hour increments, so the first entry may not represent current time. Additionally, forecast temperatures always render with ° while current weather uses °C/°F, which can be confusing when imperial is selected.

// Get hourly forecast (next 8 entries from 3-hour forecast)
export async function getHourlyForecast(
  lat: number,
  lng: number,
  tz: string,
  locale: string,
): Promise<ForecastItem[]> {
  try {
    const apiKey = getSetting<string>('openweathermap_api_key')
    if (!apiKey) {
      return []
    }

    const unit = getMeasurementUnit()

    const response = await fetch(
      `https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lng}&units=${unit}&cnt=8&appid=${apiKey}`,
    )

    if (!response.ok) {
      console.warn(
        'Failed to get forecast data: OpenWeatherMap API responded with',
        response.status,
        response.statusText,
      )
      return []
    }

    const data = await response.json()

    if (data.cod !== '200' || !Array.isArray(data.list)) {
      return []
    }

    return data.list.map(
      (
        item: {
          dt: number
          main: { temp: number }
          weather: { id: number; description: string }[]
        },
        index: number,
      ) => {
        const temperature = Math.round(item.main.temp)
        const weatherId = item.weather?.[0]?.id ?? 800
        const description = item.weather?.[0]?.description || 'Weather'
        const iconSrc = getIconSrc(weatherId, item.dt, tz)

        const timeLabel =
          index === 0
            ? 'NOW'
            : new Intl.DateTimeFormat(locale, {
                hour: '2-digit',
                minute: '2-digit',
                hour12: false,
                timeZone: tz,
              }).format(new Date(item.dt * 1000))

        return {
          temperature,
          iconSrc,
          iconAlt: description,
          timeLabel,
          displayTemp: `${temperature}°`,
        }
      },
    )

@github-actions
Copy link

github-actions bot commented Feb 8, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix zero-temperature validation

Boolean(data.main?.temp) incorrectly rejects valid temperatures of 0 (e.g., 0°C).
This can cause the app to treat a successful API response as invalid and render
nothing. Check for a numeric value instead of truthiness.

edge-apps/weather-new/src/weather.ts [26-31]

 function isValidWeatherResponse(data: {
   cod?: number | string
   main?: { temp?: number }
 }): boolean {
-  return (data.cod === 200 || data.cod === '200') && Boolean(data.main?.temp)
+  return (
+    (data.cod === 200 || data.cod === '200') &&
+    typeof data.main?.temp === 'number'
+  )
 }
Suggestion importance[1-10]: 8

__

Why: The current check Boolean(data.main?.temp) will incorrectly reject valid temperatures of 0, causing getCurrentWeather() to return null even for a successful response. Switching to a numeric type check fixes a real correctness bug with high user impact in cold climates.

Medium
Prevent NaN temps and wrong icons

temp_max/temp_min are not validated and may be missing, which will produce NaN in
the UI. Also, using Date.now() for dt can yield incorrect day/night icon selection
compared to the API's reported timestamp. Derive dt from data.dt when available and
fallback tempHigh/tempLow to temp.

edge-apps/weather-new/src/weather.ts [71-82]

 const temperature = Math.round(data.main.temp)
-const tempHigh = Math.round(data.main.temp_max)
-const tempLow = Math.round(data.main.temp_min)
+
+const tempHigh = Math.round(
+  typeof data.main.temp_max === 'number' ? data.main.temp_max : data.main.temp,
+)
+const tempLow = Math.round(
+  typeof data.main.temp_min === 'number' ? data.main.temp_min : data.main.temp,
+)
+
 const weatherId = data.weather?.[0]?.id ?? null
 
 if (!weatherId) {
   return null
 }
 
-const dt = Math.floor(Date.now() / 1000)
+const dt =
+  typeof (data as { dt?: unknown }).dt === 'number'
+    ? (data as { dt: number }).dt
+    : Math.floor(Date.now() / 1000)
+
 const description = data.weather?.[0]?.description || ''
 const iconSrc = getIconSrc(weatherId, dt, tz)
Suggestion importance[1-10]: 7

__

Why: Guarding temp_max/temp_min avoids propagating NaN into tempHigh/tempLow if those fields are missing or malformed, improving robustness. Using data.dt instead of Date.now() can make getIconSrc() choose day/night icons more accurately for the API’s reported observation time.

Medium
Security
Avoid unsafe HTML injection

Writing API-derived values into innerHTML can enable DOM XSS if any field contains
unexpected markup (e.g., compromised upstream or tampered network). Build elements
with textContent and set src/alt via properties instead of string interpolation.
This also avoids accidental broken HTML when strings contain quotes.

edge-apps/weather-new/src/main.ts [33-47]

 function renderForecastItems(items: ForecastItem[]) {
   if (!forecastItemsEl) return
 
-  forecastItemsEl.innerHTML = items
-    .map(
-      (item) => `
-      <div class="forecast-item">
-        <span class="forecast-item-temp">${item.displayTemp}</span>
-        <img class="forecast-item-icon" src="${item.iconSrc}" alt="${item.iconAlt}" />
-        <span class="forecast-item-time">${item.timeLabel}</span>
-      </div>
-    `,
-    )
-    .join('')
+  const container = forecastItemsEl as HTMLElement
+  container.replaceChildren()
+
+  for (const item of items) {
+    const row = document.createElement('div')
+    row.className = 'forecast-item'
+
+    const temp = document.createElement('span')
+    temp.className = 'forecast-item-temp'
+    temp.textContent = item.displayTemp
+
+    const img = document.createElement('img')
+    img.className = 'forecast-item-icon'
+    img.src = item.iconSrc
+    img.alt = item.iconAlt
+
+    const time = document.createElement('span')
+    time.className = 'forecast-item-time'
+    time.textContent = item.timeLabel
+
+    row.append(temp, img, time)
+    container.appendChild(row)
+  }
 }
Suggestion importance[1-10]: 8

__

Why: Writing API-derived values via innerHTML creates an avoidable DOM XSS surface and can break markup if strings contain special characters. Building DOM nodes and assigning textContent/src/alt directly is a safer, correct alternative for rendering ForecastItem data.

Medium

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a redesigned “weather-new” Edge App implemented in TypeScript, using @screenly/edge-apps utilities and web components to render current conditions and an 8-item forecast with updated styling and a new icon set.

Changes:

  • Added TypeScript modules for fetching current weather, hourly forecast, and reverse-geocoded location using OpenWeatherMap.
  • Added a full set of SVG weather icons and a key→asset mapping used by the weather logic.
  • Added a new UI (HTML + CSS) and app manifests/scripts to build and run the app with Bun tooling.

Reviewed changes

Copilot reviewed 12 out of 35 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
edge-apps/weather-new/static/images/icons/windy.svg Adds new windy icon asset.
edge-apps/weather-new/static/images/icons/thunderstorm.svg Adds new thunderstorm icon asset.
edge-apps/weather-new/static/images/icons/thunderstorm-night.svg Adds new thunderstorm-night icon asset.
edge-apps/weather-new/static/images/icons/snow.svg Adds new snow icon asset.
edge-apps/weather-new/static/images/icons/sleet.svg Adds new sleet icon asset.
edge-apps/weather-new/static/images/icons/sleet-night.svg Adds new sleet-night icon asset.
edge-apps/weather-new/static/images/icons/rainy.svg Adds new rainy icon asset.
edge-apps/weather-new/static/images/icons/rain-night.svg Adds new rain-night icon asset.
edge-apps/weather-new/static/images/icons/partlysunny.svg Adds new partly-sunny icon asset.
edge-apps/weather-new/static/images/icons/partially-cloudy.svg Adds new partially-cloudy icon asset.
edge-apps/weather-new/static/images/icons/partially-cloudy-night.svg Adds new partially-cloudy-night icon asset.
edge-apps/weather-new/static/images/icons/mostly-cloudy.svg Adds new mostly-cloudy icon asset.
edge-apps/weather-new/static/images/icons/mostly-cloudy-night.svg Adds new mostly-cloudy-night icon asset.
edge-apps/weather-new/static/images/icons/haze.svg Adds new haze icon asset.
edge-apps/weather-new/static/images/icons/fog.svg Adds new fog icon asset.
edge-apps/weather-new/static/images/icons/fewdrops.svg Adds new “few drops” icon asset.
edge-apps/weather-new/static/images/icons/drizzle.svg Adds new drizzle icon asset.
edge-apps/weather-new/static/images/icons/cloudy.svg Adds new cloudy icon asset.
edge-apps/weather-new/static/images/icons/clear.svg Adds new clear-day icon asset.
edge-apps/weather-new/static/images/icons/clear-night.svg Adds new clear-night icon asset.
edge-apps/weather-new/static/images/icons/chancesleet.svg Adds new chance-sleet icon asset.
edge-apps/weather-new/src/weather.ts Implements current weather + forecast fetching/parsing and icon selection.
edge-apps/weather-new/src/weather-icons.ts Maps icon keys to bundled SVG assets.
edge-apps/weather-new/src/settings.ts Adds measurement-unit setting accessor.
edge-apps/weather-new/src/main.ts App bootstrap: reads metadata/locale/timezone, renders UI, refresh loop.
edge-apps/weather-new/src/location.ts Reverse-geocodes coordinates to city name via OpenWeatherMap.
edge-apps/weather-new/src/css/style.css Adds glassmorphic layout/styling for current weather + forecast card.
edge-apps/weather-new/screenly_qc.yml Adds QC manifest/settings for the new app.
edge-apps/weather-new/screenly.yml Adds production manifest/settings for the new app.
edge-apps/weather-new/package.json Adds Bun/build/lint/typecheck scripts and workspace dependency wiring.
edge-apps/weather-new/index.html Adds app HTML shell with Screenly components and dist asset references.
edge-apps/weather-new/bun.lock Adds lockfile for the new app’s dependencies.
edge-apps/weather-new/README.md Documents settings and dev/build/deploy workflows.
edge-apps/weather-new/.gitignore Ignores build artifacts and dependencies.

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

Comment on lines +36 to +41
'partially-cloudy-night': partiallyCloudyNightIcon,
partlysunny: partlySunnyIcon,
rainy: rainyIcon,
'rain-night': rainNightIcon,
sleet: sleetIcon,
'sleet-night': sleetNightIcon,
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

getWeatherIconKey() from @screenly/edge-apps returns the key rain for IDs 500–599, but this map defines rainy instead. As a result, rainy conditions will fall back to clear and display the wrong icon. Add a rain entry (and ensure keys match all possible getWeatherIconKey outputs) or rename the key to rain.

Copilot uses AI. Check for mistakes.
cod?: number | string
main?: { temp?: number }
}): boolean {
return (data.cod === 200 || data.cod === '200') && Boolean(data.main?.temp)
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

isValidWeatherResponse() uses Boolean(data.main?.temp), which rejects valid temperatures of 0 (e.g., 0°C). This will cause the app to treat a valid API response as invalid and show no weather. Prefer checking data.main?.temp !== undefined (and/or Number.isFinite) instead of truthiness.

Suggested change
return (data.cod === 200 || data.cod === '200') && Boolean(data.main?.temp)
const hasValidCode = data.cod === 200 || data.cod === '200'
const temp = data.main?.temp
const hasValidTemp = typeof temp === 'number' && Number.isFinite(temp)
return hasValidCode && hasValidTemp

Copilot uses AI. Check for mistakes.
Comment on lines +71 to +74
const temperature = Math.round(data.main.temp)
const tempHigh = Math.round(data.main.temp_max)
const tempLow = Math.round(data.main.temp_min)
const weatherId = data.weather?.[0]?.id ?? null
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

getCurrentWeather() rounds data.main.temp_max/temp_min without validating they exist. If the API response is missing these fields (or they are non-numeric), Math.round will produce NaN and the UI will render NaN°. Consider extending response validation to include temp_min/temp_max (or defaulting them to temperature).

Copilot uses AI. Check for mistakes.

const data = await response.json()

if (data.cod !== '200' || !Array.isArray(data.list)) {
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

getHourlyForecast() only accepts data.cod === '200', but earlier you note cod can be a number or string. If this endpoint ever returns 200 as a number, the function will incorrectly return an empty forecast. Consider accepting both 200 and '200' for consistency.

Suggested change
if (data.cod !== '200' || !Array.isArray(data.list)) {
if ((data.cod !== '200' && data.cod !== 200) || !Array.isArray(data.list)) {

Copilot uses AI. Check for mistakes.
Comment on lines +36 to +46
forecastItemsEl.innerHTML = items
.map(
(item) => `
<div class="forecast-item">
<span class="forecast-item-temp">${item.displayTemp}</span>
<img class="forecast-item-icon" src="${item.iconSrc}" alt="${item.iconAlt}" />
<span class="forecast-item-time">${item.timeLabel}</span>
</div>
`,
)
.join('')
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

renderForecastItems() builds HTML via innerHTML and interpolates item.iconAlt (from the weather API) directly into an attribute. Even if OpenWeatherMap normally returns safe strings, this is still untrusted input and can lead to HTML/attribute injection. Prefer creating elements and setting textContent/setAttribute, or escape/sanitize interpolated values before assigning to innerHTML.

Suggested change
forecastItemsEl.innerHTML = items
.map(
(item) => `
<div class="forecast-item">
<span class="forecast-item-temp">${item.displayTemp}</span>
<img class="forecast-item-icon" src="${item.iconSrc}" alt="${item.iconAlt}" />
<span class="forecast-item-time">${item.timeLabel}</span>
</div>
`,
)
.join('')
// Clear any existing forecast items
forecastItemsEl.innerHTML = ''
for (const item of items) {
const itemEl = document.createElement('div')
itemEl.className = 'forecast-item'
const tempEl = document.createElement('span')
tempEl.className = 'forecast-item-temp'
tempEl.textContent = item.displayTemp
const iconEl = document.createElement('img')
iconEl.className = 'forecast-item-icon'
iconEl.setAttribute('src', item.iconSrc)
iconEl.setAttribute('alt', item.iconAlt)
const timeEl = document.createElement('span')
timeEl.className = 'forecast-item-time'
timeEl.textContent = item.timeLabel
itemEl.appendChild(tempEl)
itemEl.appendChild(iconEl)
itemEl.appendChild(timeEl)
forecastItemsEl.appendChild(itemEl)
}

Copilot uses AI. Check for mistakes.
optional: true
help_text: |
Override the default locale with a supported language code (e.g., en, fr, de). Defaults to English if not specified.
You can find the available locales here: https://momentjs.com/
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

This manifest includes override_locale/override_coordinates, but the app also depends on settings not declared here: getTimeZone() supports override_timezone, and getMeasurementUnit() reads unit. Without declaring them, users can’t configure timezone override or imperial/metric units via settings. Consider adding override_timezone and unit (as in edge-apps/clock/screenly.yml) and either wire up override_coordinates or remove it.

Suggested change
You can find the available locales here: https://momentjs.com/
You can find the available locales here: https://momentjs.com/
override_timezone:
type: string
default_value: ''
title: Override Timezone
optional: true
help_text: |
Enter an IANA timezone identifier (e.g., Europe/Berlin, America/New_York) to override the device timezone.
If not specified, the app will use the device's configured timezone.
unit:
type: string
default_value: metric
title: Measurement Unit
optional: true
choices:
- metric
- imperial
help_text: |
Select which measurement system to use. "metric" shows temperatures in °C and wind speed in m/s or km/h.
"imperial" shows temperatures in °F and wind speed in mph.

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +45
override_coordinates:
type: string
default_value: ''
title: Override Coordinates
optional: true
help_text: |
Enter a comma-separated pair of coordinates (e.g., 37.8267,-122.4233). If not specified, the app will attempt to use the device's coordinates.
override_locale:
type: string
default_value: 'en'
title: Override Locale
optional: true
help_text: |
Override the default locale with a supported language code (e.g., en, fr, de). Defaults to English if not specified.
You can find the available locales here: https://momentjs.com/
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

Same as screenly.yml: settings declared here don’t include override_timezone/unit even though the app reads them (via getTimeZone() and getMeasurementUnit()), and override_coordinates isn’t used by the app. Align the manifest settings with what the app actually supports.

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +174
export async function getCurrentWeather(
lat: number,
lng: number,
tz: string,
): Promise<CurrentWeatherData | null> {
try {
const apiKey = getSetting<string>('openweathermap_api_key')
if (!apiKey) {
return null
}

const unit = getMeasurementUnit()

const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lng}&units=${unit}&appid=${apiKey}`,
)

if (!response.ok) {
console.warn(
'Failed to get weather data: OpenWeatherMap API responded with',
response.status,
response.statusText,
)
return null
}

const data = await response.json()

if (!isValidWeatherResponse(data)) {
return null
}

const temperature = Math.round(data.main.temp)
const tempHigh = Math.round(data.main.temp_max)
const tempLow = Math.round(data.main.temp_min)
const weatherId = data.weather?.[0]?.id ?? null

if (!weatherId) {
return null
}

const dt = Math.floor(Date.now() / 1000)
const description = data.weather?.[0]?.description || ''
const iconSrc = getIconSrc(weatherId, dt, tz)
const iconAlt = description || 'Weather icon'

const tempSymbol = unit === 'imperial' ? '°F' : '°C'

return {
temperature,
weatherId,
description: capitalizeFirstLetter(description),
iconSrc,
iconAlt,
tempHigh,
tempLow,
displayTemp: `${temperature}${tempSymbol}`,
}
} catch (error) {
console.warn('Failed to get weather data:', error)
return null
}
}

// Get hourly forecast (next 8 entries from 3-hour forecast)
export async function getHourlyForecast(
lat: number,
lng: number,
tz: string,
locale: string,
): Promise<ForecastItem[]> {
try {
const apiKey = getSetting<string>('openweathermap_api_key')
if (!apiKey) {
return []
}

const unit = getMeasurementUnit()

const response = await fetch(
`https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lng}&units=${unit}&cnt=8&appid=${apiKey}`,
)

if (!response.ok) {
console.warn(
'Failed to get forecast data: OpenWeatherMap API responded with',
response.status,
response.statusText,
)
return []
}

const data = await response.json()

if (data.cod !== '200' || !Array.isArray(data.list)) {
return []
}

return data.list.map(
(
item: {
dt: number
main: { temp: number }
weather: { id: number; description: string }[]
},
index: number,
) => {
const temperature = Math.round(item.main.temp)
const weatherId = item.weather?.[0]?.id ?? 800
const description = item.weather?.[0]?.description || 'Weather'
const iconSrc = getIconSrc(weatherId, item.dt, tz)

const timeLabel =
index === 0
? 'NOW'
: new Intl.DateTimeFormat(locale, {
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZone: tz,
}).format(new Date(item.dt * 1000))

return {
temperature,
iconSrc,
iconAlt: description,
timeLabel,
displayTemp: `${temperature}°`,
}
},
)
} catch (error) {
console.warn('Failed to get forecast data:', error)
return []
}
}
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

No unit tests are added for the new weather parsing/mapping logic (getCurrentWeather, getHourlyForecast, icon key mapping). The repo already has Bun unit tests for similar weather logic (e.g., edge-apps/clock/src/weather.test.ts), so adding tests here would help prevent regressions (especially around cod handling, 0° temps, and icon key mapping).

Copilot uses AI. Check for mistakes.
Comment on lines +76 to +82
const forecast = await getHourlyForecast(latitude, longitude, tz, locale)

if (forecast.length > 0) {
renderForecastItems(forecast)
} else {
hideForecastCard()
}
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

If the forecast request fails once, hideForecastCard() sets display: none permanently, and the card is never shown again even if a later refresh successfully returns forecast items (because the success path only renders items). Consider restoring the card display when forecast.length > 0 (e.g., clear the inline style or set display back to its default).

Copilot uses AI. Check for mistakes.
- Add getCoordinates() wrapper function to check override_coordinates setting
- Parses comma-separated coordinates and validates before using
- Falls back to metadata.coordinates if override is not specified

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant