chore: release v1.5.0
Build and Push / release (push) Successful in 3s
Build and Push / build (push) Successful in 3m12s

feat: add dynamic environment widgets

Add live weather, sun position, and forecast widgets to Front Hall dashboard,
powered by data from the Qdrant volatile collection via core-api.

New widgets:
- SunPositionWidget: Animated arc showing sun/moon position with gradient colors
- ForecastWidget: Multi-day weather outlook
- Updated WeatherWidget and AirQualityWidget to accept API data

Infrastructure:
- Environment datasource calling GET /tools/environment
- Environment provider with 5-minute auto-refresh
- Freezed models for environment data

Tests:
- 12 widget tests for environment section
- Updated existing tests with givenEnvironment() harness method

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeroen Schweitzer
2026-01-06 23:05:24 +01:00
co-authored by Claude Opus 4.5
parent ea6914b5f4
commit fffc3d5baf
14 changed files with 1469 additions and 30 deletions
+39 -3
View File
@@ -1,17 +1,24 @@
import 'package:flutter/material.dart';
import 'package:tatlock_ui/features/front_hall/data/models/environment_model.dart'
as api;
import 'package:tatlock_ui/shared/theme/stoplight_colors.dart';
/// Air Quality Index widget displaying current AQI.
///
/// Currently uses mock data. Will be connected to air quality API in future.
/// Displays air quality data from the Core API environment endpoint.
/// Only render this widget when air quality data is available.
class AirQualityWidget extends StatelessWidget {
const AirQualityWidget({
super.key,
this.apiData,
this.data,
this.compact = false,
});
/// Air quality data to display. Uses mock data if null.
/// Air quality data from API. Takes priority over legacy data.
final api.AirQualityData? apiData;
/// Legacy air quality data to display. Uses mock data if null.
final AirQualityData? data;
/// Whether to use compact layout.
@@ -20,7 +27,11 @@ class AirQualityWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final aqi = data ?? AirQualityData.mock();
// Convert API data to local model, or use legacy data
final aqi = apiData != null
? _fromApiData(apiData!)
: (data ?? AirQualityData.mock());
if (compact) {
return _buildCompact(context, colorScheme, aqi);
@@ -199,6 +210,31 @@ class _PollutantChip extends StatelessWidget {
}
}
/// Converts API air quality data to local AirQualityData model.
AirQualityData _fromApiData(api.AirQualityData data) {
final index = data.aqi ?? 0;
final pollutants = <Pollutant>[];
if (data.pm25 != null) {
pollutants.add(Pollutant(name: 'PM2.5', value: data.pm25!, unit: 'µg/m³'));
}
if (data.pm10 != null) {
pollutants.add(Pollutant(name: 'PM10', value: data.pm10!, unit: 'µg/m³'));
}
if (data.o3 != null) {
pollutants.add(Pollutant(name: 'O₃', value: data.o3!, unit: 'ppb'));
}
if (data.no2 != null) {
pollutants.add(Pollutant(name: 'NO₂', value: data.no2!, unit: 'ppb'));
}
return AirQualityData(
index: index,
level: AqiLevel.fromIndex(index),
pollutants: pollutants,
);
}
/// Air Quality Index levels based on US EPA standard.
enum AqiLevel {
good(