feat(todo): Todo's server answers a place's forecast (ADR 0033, D40)

The weather of D40 comes through the instance, as Train's workouts'
(ADR 0036, D1): Todo's server gets a `weather` endpoint, `offered` and
`forecast`, answering a place's hours for the next 16 days from
Open-Meteo for people signed in alone, the place rounded to about a
kilometre, nothing kept or logged, 30 requests a person a minute.
Refusals are `WeatherRefused` (off, invalid, tooOften, unreachable).
`WEATHER=off` and `OPEN_METEO_API_KEY` switch it as they switch Train's,
and production's compose file passes them to Todo's server too.

D40 records the developer's decisions of 10 Nov 2026 on the weather and
the agent's details (what dry, sunny, a temperature range and calm mean
over a task's hours).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-10 14:43:15 +01:00
commit a91aa98817
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -21,6 +21,8 @@ import 'package:halkyon_todo_server/src/generated/search/place.dart'
as _ih6de0l6;
import 'package:halkyon_todo_server/src/generated/search/place_point.dart'
as _iywv9say;
import 'package:halkyon_todo_server/src/generated/weather/forecast_hour.dart'
as _i9va4aku;
import 'package:serverpod/serverpod.dart' as _is;
import 'package:serverpod_test/serverpod_test.dart' as _ist;
import 'package:halkyon_todo_server/src/generated/protocol.dart';
@ -159,6 +161,8 @@ class TestEndpoints {
late final futureCalls = _FutureCalls();
late final _SearchEndpoint search;
late final _WeatherEndpoint weather;
}
class _InternalTestEndpoints extends TestEndpoints
@ -172,6 +176,10 @@ class _InternalTestEndpoints extends TestEndpoints
endpoints,
serializationManager,
);
weather = _WeatherEndpoint(
endpoints,
serializationManager,
);
}
}
@ -290,6 +298,80 @@ class _SearchEndpoint {
}
}
class _WeatherEndpoint {
_WeatherEndpoint(
this._endpointDispatch,
this._serializationManager,
);
final _is.EndpointDispatch _endpointDispatch;
final _is.SerializationManager _serializationManager;
_ida.Future<bool> offered(_ist.TestSessionBuilder sessionBuilder) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'weather',
method: 'offered',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'weather',
methodName: 'offered',
parameters: _ist.testObjectToJson({}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<bool>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
_ida.Future<List<_i9va4aku.ForecastHour>> forecast(
_ist.TestSessionBuilder sessionBuilder, {
required double lat,
required double lon,
}) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
endpoint: 'weather',
method: 'forecast',
);
try {
var _localCallContext = await _endpointDispatch.getMethodCallContext(
createSessionCallback: (_) => _localUniqueSession,
endpointPath: 'weather',
methodName: 'forecast',
parameters: _ist.testObjectToJson({
'lat': lat,
'lon': lon,
}),
serializationManager: _serializationManager,
);
var _localReturnValue =
await (_localCallContext.method.call(
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<List<_i9va4aku.ForecastHour>>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();
}
});
}
}
class _PruningFutureCall {
Future<void> pruneAccounts(_ist.TestSessionBuilder sessionBuilder) async {
var _localUniqueSession =