diff --git a/edge-apps/weather/README.md b/edge-apps/weather/README.md index d5712608e..3f84852c3 100644 --- a/edge-apps/weather/README.md +++ b/edge-apps/weather/README.md @@ -22,6 +22,7 @@ The app accepts the following settings via `screenly.yml`: | Setting | Description | Type | Default | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ------- | +| `display_errors` | For debugging purposes to display errors on the screen. | optional, advanced | `false` | | `openweathermap_api_key` | OpenWeatherMap API key to access weather data and location information. Get your API key from the [OpenWeatherMap API](https://openweathermap.org/api) | required | - | | `override_coordinates` | Comma-separated coordinates (e.g., `37.8267,-122.4233`) to override device location | optional | - | | `override_locale` | Override the default locale with a supported language code | optional | `en` | diff --git a/edge-apps/weather/e2e/screenshots.spec.ts b/edge-apps/weather/e2e/screenshots.spec.ts index fdb9fb43a..e11a66d55 100644 --- a/edge-apps/weather/e2e/screenshots.spec.ts +++ b/edge-apps/weather/e2e/screenshots.spec.ts @@ -20,12 +20,26 @@ const { screenlyJsContent } = createMockScreenlyForScreenshots( location: 'Mountain View, CA', }, { + display_errors: 'false', override_timezone: 'America/Los_Angeles', override_locale: 'en', openweathermap_api_key: 'mock-api-key', }, ) +const { screenlyJsContent: screenlyJsContentNoApiKey } = + createMockScreenlyForScreenshots( + { + coordinates: [37.3893889, -122.0832101], + location: 'Mountain View, CA', + }, + { + display_errors: 'false', + override_timezone: 'America/Los_Angeles', + override_locale: 'en', + }, + ) + for (const { width, height } of RESOLUTIONS) { test(`screenshot ${width}x${height}`, async ({ browser }) => { const screenshotsDir = getScreenshotsDir() @@ -53,3 +67,30 @@ for (const { width, height } of RESOLUTIONS) { await context.close() }) } + +const NO_API_KEY_RESOLUTIONS = [ + { width: 3840, height: 2160 }, + { width: 2160, height: 3840 }, +] + +for (const { width, height } of NO_API_KEY_RESOLUTIONS) { + test(`screenshot no-api-key ${width}x${height}`, async ({ browser }) => { + const screenshotsDir = getScreenshotsDir() + + const context = await browser.newContext({ viewport: { width, height } }) + const page = await context.newPage() + + await setupClockMock(page) + await setupScreenlyJsMock(page, screenlyJsContentNoApiKey) + + await page.goto('/') + await page.waitForLoadState('networkidle') + + await page.screenshot({ + path: path.join(screenshotsDir, `no-api-key-${width}x${height}.png`), + fullPage: false, + }) + + await context.close() + }) +} diff --git a/edge-apps/weather/index.html b/edge-apps/weather/index.html index 420d4281c..0b706be42 100644 --- a/edge-apps/weather/index.html +++ b/edge-apps/weather/index.html @@ -41,6 +41,22 @@
+ + diff --git a/edge-apps/weather/screenly.yml b/edge-apps/weather/screenly.yml index 77f509379..fb2e808f1 100644 --- a/edge-apps/weather/screenly.yml +++ b/edge-apps/weather/screenly.yml @@ -5,9 +5,20 @@ description: Displays the current weather and time icon: https://playground.srly.io/edge-apps/weather/static/img/icon.svg author: Screenly, Inc. categories: - - Utilities +- Utilities ready_signal: true settings: + display_errors: + type: string + default_value: 'false' + title: Display Errors + optional: true + help_text: + properties: + advanced: true + help_text: For debugging purposes to display errors on the screen. + type: boolean + schema_version: 1 enable_analytics: type: string default_value: 'true' diff --git a/edge-apps/weather/screenly_qc.yml b/edge-apps/weather/screenly_qc.yml index eaacbe9a9..a63f150e9 100644 --- a/edge-apps/weather/screenly_qc.yml +++ b/edge-apps/weather/screenly_qc.yml @@ -5,9 +5,20 @@ description: Displays the current weather and time icon: https://playground.srly.io/edge-apps/weather/static/img/icon.svg author: Screenly, Inc. categories: - - Utilities +- Utilities ready_signal: true settings: + display_errors: + type: string + default_value: 'false' + title: Display Errors + optional: true + help_text: + properties: + advanced: true + help_text: For debugging purposes to display errors on the screen. + type: boolean + schema_version: 1 enable_analytics: type: string default_value: 'true' diff --git a/edge-apps/weather/screenshots/1080x1920.webp b/edge-apps/weather/screenshots/1080x1920.webp index d5785f6e0..fbac4b640 100644 Binary files a/edge-apps/weather/screenshots/1080x1920.webp and b/edge-apps/weather/screenshots/1080x1920.webp differ diff --git a/edge-apps/weather/screenshots/1280x720.webp b/edge-apps/weather/screenshots/1280x720.webp index 81872c516..0ebd9b0fd 100644 Binary files a/edge-apps/weather/screenshots/1280x720.webp and b/edge-apps/weather/screenshots/1280x720.webp differ diff --git a/edge-apps/weather/screenshots/1920x1080.webp b/edge-apps/weather/screenshots/1920x1080.webp index da054942b..56004a4ce 100644 Binary files a/edge-apps/weather/screenshots/1920x1080.webp and b/edge-apps/weather/screenshots/1920x1080.webp differ diff --git a/edge-apps/weather/screenshots/2160x3840.webp b/edge-apps/weather/screenshots/2160x3840.webp index e27b8474e..c35fc8937 100644 Binary files a/edge-apps/weather/screenshots/2160x3840.webp and b/edge-apps/weather/screenshots/2160x3840.webp differ diff --git a/edge-apps/weather/screenshots/2160x4096.webp b/edge-apps/weather/screenshots/2160x4096.webp index 5b100f44f..dd1389c8b 100644 Binary files a/edge-apps/weather/screenshots/2160x4096.webp and b/edge-apps/weather/screenshots/2160x4096.webp differ diff --git a/edge-apps/weather/screenshots/3840x2160.webp b/edge-apps/weather/screenshots/3840x2160.webp index fd13eb079..6e08b695a 100644 Binary files a/edge-apps/weather/screenshots/3840x2160.webp and b/edge-apps/weather/screenshots/3840x2160.webp differ diff --git a/edge-apps/weather/screenshots/4096x2160.webp b/edge-apps/weather/screenshots/4096x2160.webp index f11b795bb..779ff6ad5 100644 Binary files a/edge-apps/weather/screenshots/4096x2160.webp and b/edge-apps/weather/screenshots/4096x2160.webp differ diff --git a/edge-apps/weather/screenshots/480x800.webp b/edge-apps/weather/screenshots/480x800.webp index 7207677ab..6715777dc 100644 Binary files a/edge-apps/weather/screenshots/480x800.webp and b/edge-apps/weather/screenshots/480x800.webp differ diff --git a/edge-apps/weather/screenshots/720x1280.webp b/edge-apps/weather/screenshots/720x1280.webp index 7c33b616e..ddd7c2e69 100644 Binary files a/edge-apps/weather/screenshots/720x1280.webp and b/edge-apps/weather/screenshots/720x1280.webp differ diff --git a/edge-apps/weather/screenshots/800x480.webp b/edge-apps/weather/screenshots/800x480.webp index 2cbe5685a..eb8401195 100644 Binary files a/edge-apps/weather/screenshots/800x480.webp and b/edge-apps/weather/screenshots/800x480.webp differ diff --git a/edge-apps/weather/screenshots/no-api-key-2160x3840.webp b/edge-apps/weather/screenshots/no-api-key-2160x3840.webp new file mode 100644 index 000000000..8e2c671a6 Binary files /dev/null and b/edge-apps/weather/screenshots/no-api-key-2160x3840.webp differ diff --git a/edge-apps/weather/screenshots/no-api-key-3840x2160.webp b/edge-apps/weather/screenshots/no-api-key-3840x2160.webp new file mode 100644 index 000000000..b2d4fbdc2 Binary files /dev/null and b/edge-apps/weather/screenshots/no-api-key-3840x2160.webp differ diff --git a/edge-apps/weather/src/css/style.css b/edge-apps/weather/src/css/style.css index 16b132c42..c06918432 100644 --- a/edge-apps/weather/src/css/style.css +++ b/edge-apps/weather/src/css/style.css @@ -193,6 +193,93 @@ auto-scaler { color: #e9e9e9; } +/* Error screen */ + +#error-screen { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 3rem; + z-index: 10; +} + +.error-card { + background: linear-gradient( + 106deg, + rgba(255, 255, 255, 0.09) 3%, + rgba(255, 255, 255, 0.01) 100% + ); + border: 1.054px solid rgba(255, 255, 255, 0.1); + border-radius: 1rem; + display: flex; + flex-direction: row; + overflow: hidden; + max-width: 60rem; + width: 100%; + backdrop-filter: blur(31.63px); + box-shadow: + 0 1px 0 0 rgba(255, 255, 255, 0.15) inset, + 0 -1px 0 0 rgba(255, 255, 255, 0.05) inset; +} + +.error-card-left { + background: rgba(255, 255, 255, 0.08); + border-right: 0.0625rem solid rgba(255, 255, 255, 0.1); + padding: 2.5rem 2rem; + display: flex; + flex-direction: column; + justify-content: flex-start; + gap: 1rem; + flex: 0 0 20rem; +} + +.error-card-left h2 { + font-size: 2rem; + font-weight: 700; + line-height: 1.2; + letter-spacing: -0.03em; + color: rgba(255, 255, 255, 0.95); + margin: 0; + text-shadow: none; +} + +.error-card-left p { + font-size: 1.25rem; + color: rgba(255, 255, 255, 0.85); + line-height: 1.6; + margin: 0; + text-shadow: none; +} + +.error-card-right { + flex: 1; + padding: 2.5rem 2rem; + display: flex; + flex-direction: column; + justify-content: flex-start; + gap: 0.75rem; +} + +.error-detail-label { + font-size: 0.75rem; + font-weight: 600; + color: rgba(255, 255, 255, 0.5); + text-transform: uppercase; + letter-spacing: 0.08em; + text-shadow: none; +} + +.error-detail-message { + font-size: 1.25rem; + color: rgba(255, 255, 255, 0.85); + line-height: 1.6; + word-break: break-word; + margin: 0; + text-shadow: none; +} + /* Portrait mode */ @media (orientation: portrait) { .current-weather { @@ -236,4 +323,19 @@ auto-scaler { .forecast-item-time-period { display: none; } + + #error-screen { + padding: 1.5rem 2rem; + } + + .error-card { + flex-direction: column; + max-width: 100%; + } + + .error-card-left { + flex: none; + border-right: none; + border-bottom: 0.0625rem solid rgba(255, 255, 255, 0.1); + } } diff --git a/edge-apps/weather/src/main.ts b/edge-apps/weather/src/main.ts index e0aceda33..e31513fd5 100644 --- a/edge-apps/weather/src/main.ts +++ b/edge-apps/weather/src/main.ts @@ -8,14 +8,41 @@ import { getSetting, getCityInfo, resolveMeasurementUnit, + setupErrorHandling, type MeasurementUnit, } from '@screenly/edge-apps' import '@screenly/edge-apps/components' -import { getCurrentWeather, getHourlyForecast } from './weather' +import { + getCurrentWeather, + getHourlyForecast, + MISSING_API_KEY_ERROR, +} from './weather' import type { ForecastItem } from './weather' import { updateBackground } from './background' import sunIcon from '../static/images/sun.svg' +type ErrorReporter = (error: unknown) => void + +function showError(error: unknown): void { + const message = error instanceof Error ? error.message : String(error) + const contentEl = document.querySelector('main.content') + const errorScreen = document.getElementById('error-screen') + const errorMessage = document.getElementById('error-message') + + if (contentEl) contentEl.style.display = 'none' + if (errorScreen) errorScreen.style.display = 'flex' + if (errorMessage) errorMessage.textContent = message +} + +function createErrorReporter(displayErrors: boolean): ErrorReporter { + if (displayErrors) { + return (error) => { + throw error instanceof Error ? error : new Error(String(error)) + } + } + return showError +} + // DOM elements let locationEl: Element | null let temperatureEl: Element | null @@ -146,19 +173,28 @@ async function updateWeatherDisplay( } document.addEventListener('DOMContentLoaded', async () => { + setupErrorHandling() + + locationEl = document.querySelector('[data-location]') + temperatureEl = document.querySelector('[data-temperature]') + weatherDescriptionEl = document.querySelector('[data-weather-description]') + tempHighEl = document.querySelector('[data-temp-high]') + tempLowEl = document.querySelector('[data-temp-low]') + forecastItemsEl = document.querySelector('[data-forecast-items]') + forecastCardEl = document.querySelector('[data-forecast-card]') + forecastHeaderIconEl = document.querySelector('[data-forecast-header-icon]') + + if (forecastHeaderIconEl) { + forecastHeaderIconEl.src = sunIcon + } + + const displayErrors = getSetting('display_errors') === 'true' + const reportError = createErrorReporter(displayErrors) + try { - locationEl = document.querySelector('[data-location]') - temperatureEl = document.querySelector('[data-temperature]') - weatherDescriptionEl = document.querySelector('[data-weather-description]') - tempHighEl = document.querySelector('[data-temp-high]') - tempLowEl = document.querySelector('[data-temp-low]') - forecastItemsEl = document.querySelector('[data-forecast-items]') - forecastCardEl = document.querySelector('[data-forecast-card]') - forecastHeaderIconEl = document.querySelector('[data-forecast-header-icon]') - - // Set forecast header icon - if (forecastHeaderIconEl) { - forecastHeaderIconEl.src = sunIcon + const apiKey = getSetting('openweathermap_api_key') + if (!apiKey) { + throw new Error(MISSING_API_KEY_ERROR) } const [latitude, longitude] = getCoordinates() @@ -184,8 +220,9 @@ document.addEventListener('DOMContentLoaded', async () => { 15 * 60 * 1000, ) } catch (error) { - console.error('Failed to initialize app:', error) + console.error('Weather app initialization failed:', error) + reportError(error) + } finally { + signalReady() } - - signalReady() }) diff --git a/edge-apps/weather/src/weather.test.ts b/edge-apps/weather/src/weather.test.ts index 2b89d582b..6e424dae3 100644 --- a/edge-apps/weather/src/weather.test.ts +++ b/edge-apps/weather/src/weather.test.ts @@ -1,6 +1,10 @@ import '@screenly/edge-apps/test' import { describe, test, expect } from 'bun:test' -import { getCurrentWeather, getHourlyForecast } from './weather' +import { + getCurrentWeather, + getHourlyForecast, + MISSING_API_KEY_ERROR, +} from './weather' let mockFetchCurrentWeatherData: ( lat: number, @@ -204,19 +208,19 @@ describe('getHourlyForecast', () => { displayTemp: '58°F', } - test('should return empty array when no API key', async () => { + test('should throw when no API key is provided', async () => { mockGetSetting = () => undefined - const result = await getHourlyForecast( - 37.39, - -122.0812, - 'America/Los_Angeles', - 'en', - 'imperial', - mockCurrentWeather, - ) - - expect(result).toEqual([]) + await expect( + getHourlyForecast( + 37.39, + -122.0812, + 'America/Los_Angeles', + 'en', + 'imperial', + mockCurrentWeather, + ), + ).rejects.toThrow(MISSING_API_KEY_ERROR) }) test('should prepend current weather as NOW and return forecast items', async () => { diff --git a/edge-apps/weather/src/weather.ts b/edge-apps/weather/src/weather.ts index 090826564..85ba2b1f3 100644 --- a/edge-apps/weather/src/weather.ts +++ b/edge-apps/weather/src/weather.ts @@ -6,6 +6,9 @@ import { type MeasurementUnit, } from '@screenly/edge-apps' +export const MISSING_API_KEY_ERROR = + 'OpenWeatherMap API key is required. Please configure it in the app settings.' + export interface CurrentWeatherData { temperature: number weatherId: number @@ -59,12 +62,12 @@ export async function getHourlyForecast( unit: MeasurementUnit, currentWeather: CurrentWeatherData | null, ): Promise { - try { - const apiKey = getSetting('openweathermap_api_key') - if (!apiKey) { - return [] - } + const apiKey = getSetting('openweathermap_api_key') + if (!apiKey) { + throw new Error(MISSING_API_KEY_ERROR) + } + try { const response = await fetch( `https://api.openweathermap.org/data/2.5/forecast?lat=${lat}&lon=${lng}&units=${unit}&cnt=7&appid=${apiKey}`, ) @@ -125,7 +128,7 @@ export async function getHourlyForecast( return forecastItems } catch (error) { - console.warn('Failed to get forecast data:', error) + console.error('Failed to get forecast data:', error) return [] } }