# Phase 2: New Widget Services & APIs **Status:** ✅ Complete **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** backend ## Objective Implement backend services and API routes for all 8 new widget types. Each widget type that fetches external data needs a dedicated service for data fetching, caching, and error handling. ## Tasks ### 2.1 Clock/Weather service - [x] Create `src/lib/server/services/weatherService.ts` - Fetch weather from OpenMeteo API (no API key required): `https://api.open-meteo.com/v1/forecast` - Accept latitude/longitude, return current temp, condition, icon - Cache responses for 30 minutes (in-memory Map with TTL) - Graceful fallback when API is unreachable - [x] Create `src/routes/api/widgets/weather/+server.ts` — GET endpoint with lat/lng query params ### 2.2 System Stats service - [x] Create `src/lib/server/services/systemStatsService.ts` - Adapter pattern: `TrueNasAdapter`, `GlancesAdapter`, `CustomAdapter` - Each adapter fetches metrics (CPU, RAM, disk) from its source URL - Return normalized `{ metric: string, value: number, unit: string }[]` - Configurable refresh interval, in-memory cache per source URL - [x] Create `src/routes/api/widgets/system-stats/+server.ts` — GET with sourceUrl, sourceType params ### 2.3 RSS/Feed service - [x] Create `src/lib/server/services/rssFeedService.ts` - Fetch and parse RSS/Atom feeds (use built-in XML parsing or lightweight lib) - Return `{ title, link, pubDate, summary }[]` limited to maxItems - Cache feeds for 15 minutes per URL - Handle malformed feeds gracefully - [x] Create `src/routes/api/widgets/rss/+server.ts` — GET with feedUrl, maxItems params ### 2.4 Calendar service - [x] Create `src/lib/server/services/calendarService.ts` - Fetch and parse iCal (.ics) files from URLs - Extract events within daysAhead range - Return `{ summary, start, end, location?, calendarLabel, calendarColor }[]` - Cache per URL for 30 minutes - Handle multiple calendar URLs, merge and sort by start time - [x] Create `src/routes/api/widgets/calendar/+server.ts` — POST with icalUrls[], daysAhead ### 2.5 Metric/Counter service - [x] Create `src/lib/server/services/metricService.ts` - `fetchHttpMetric(url, jsonPath)` — fetch JSON endpoint, extract value via JSONPath - `fetchPrometheusMetric(url, query)` — query Prometheus API, extract instant value - `getStaticMetric(value)` — passthrough for static values - Store previous value for trend calculation (up/down/flat) - Cache per source for configurable interval - [x] Create `src/routes/api/widgets/metric/+server.ts` — GET with source type params ### 2.6 Camera/Stream proxy - [x] Create `src/lib/server/services/cameraService.ts` - `fetchSnapshot(url)` — proxy HTTP request to camera URL, return image buffer - Validate URL is http/https only - Timeout after 10s - Rate limit: max 1 request per 5s per URL - [x] Create `src/routes/api/widgets/camera/+server.ts` — GET with streamUrl param, returns proxied image ### 2.7 Widget data aggregation endpoint - [x] Create `src/routes/api/widgets/data/+server.ts` — generic endpoint that routes to the correct service based on widget type and config - POST with `{ widgetType, config }` body - Routes to weatherService, systemStatsService, etc. based on type - Returns unified response format ### 2.8 Update existing widget service - [x] Update `src/lib/server/services/boardService.ts` to handle new widget types in create/update operations - [x] Ensure new widget type configs are validated with the correct Zod schema on create/update ## Files to Modify/Create - `src/lib/server/services/weatherService.ts` — new - `src/lib/server/services/systemStatsService.ts` — new - `src/lib/server/services/rssFeedService.ts` — new - `src/lib/server/services/calendarService.ts` — new - `src/lib/server/services/metricService.ts` — new - `src/lib/server/services/cameraService.ts` — new - `src/routes/api/widgets/weather/+server.ts` — new - `src/routes/api/widgets/system-stats/+server.ts` — new - `src/routes/api/widgets/rss/+server.ts` — new - `src/routes/api/widgets/calendar/+server.ts` — new - `src/routes/api/widgets/metric/+server.ts` — new - `src/routes/api/widgets/camera/+server.ts` — new - `src/routes/api/widgets/data/+server.ts` — new - `src/lib/server/services/boardService.ts` — modify ## Acceptance Criteria - Each service handles errors gracefully (network failures, malformed responses, timeouts) - In-memory caching prevents excessive external API calls - All API routes use consistent envelope response format - All user inputs validated with Zod schemas from Phase 1 - Camera proxy validates URLs and prevents SSRF (allowlist http/https, no private IPs) - Services are stateless (cache is ephemeral, no DB state needed for widget data) ## Notes - OpenMeteo API is free, no key needed: `https://api.open-meteo.com/v1/forecast?latitude=X&longitude=Y¤t_weather=true` - For RSS parsing, consider using a lightweight approach (DOMParser or regex) to avoid adding a heavy dependency. If needed, `fast-xml-parser` is a good lightweight option. - iCal parsing: use `node-ical` or hand-parse VEVENT blocks - JSONPath extraction: use simple dot-notation traversal rather than a full JSONPath library - SSRF protection for camera proxy: reject private IP ranges (10.x, 172.16-31.x, 192.168.x, 127.x, ::1) ## Review Checklist - [x] All tasks completed - [x] Code follows project conventions - [ ] No unintended side effects - [ ] Build passes (Big Bang: code quality check only) - [ ] Tests pass (Big Bang: skipped for intermediate phase) ## Handoff to Next Phase ### What was done - Created 6 new backend services: weatherService, systemStatsService, rssFeedService, calendarService, metricService, cameraService - Created 7 new API routes under `/api/widgets/`: weather, system-stats, rss, calendar, metric, camera, data (aggregation) - Updated boardService to validate widget configs against Zod schemas on create/update - Updated boardService to pass through new theme/visual fields (themeHue, themeSaturation, backgroundType, cardSize, wallpaperUrl, wallpaperBlur, wallpaperOverlay, customCss) and section cardSize - All services use in-memory caching with TTL (Map + expiry timestamps) - Camera proxy includes SSRF protection (blocks private IPs, localhost, link-local) and rate limiting (1 req/5s per URL) - RSS service uses lightweight regex-based XML parsing (no external dependency) - Calendar service uses hand-parsed VEVENT blocks from iCal text (no external dependency) - Metric service supports dot-notation JSONPath extraction (no external dependency) ### What the next phase needs to know - All widget data endpoints follow the pattern: `/api/widgets/{type}` with GET (or POST for calendar) - The aggregation endpoint `/api/widgets/data` accepts POST with `{ widgetType, config }` and routes to the correct service - Camera endpoint returns raw image binary (not JSON envelope) for direct `` src usage - Markdown and LinkGroup widget types return no-op from the aggregation endpoint (they are client-side only) - Clock widget without weather enabled also returns no-op (time is client-side) - All services export a `clearCache()` function for testing/manual refresh - The `validateStreamUrl()` function on cameraService is exported for reuse (used by aggregation endpoint) ### Potential concerns - RSS/Atom XML parsing uses regex, which handles common feeds but may fail on exotic feed formats. If issues arise, consider adding `fast-xml-parser` as a dependency. - iCal parsing handles standard VEVENT blocks but does not support RRULE (recurring events). A future enhancement could add recurrence expansion. - SSRF protection checks IP format only at the URL level — DNS rebinding attacks could bypass hostname checks. For production hardening, consider resolving DNS before connecting. - System stats adapters assume specific API shapes for Glances and Prometheus. Custom adapter is a generic JSON fallback.