feat: platform backbone and field capture, first part (checkpoint)
Platform backbone (plan 2026-10-13-plattform-rueckgrat), reviewed tasks: - run roles api/worker/job/all with shared startup checks (Task 1) - schema and migration for DomainEvent, deliveries, JobRun, ThrottlerHit (Task 2) - audit, notifications and domain events written in the same transaction (Task 3) - dispatcher with SKIP LOCKED claims, retries and stale release (Task 4) - worker building blocks: tick, LISTEN/NOTIFY wake-up, health (Task 5) - worker process and entry points (Task 6) - throttler counters in Postgres (Task 19) - separate keys per purpose with key ring (Task 20) - public origin page without exact location (Task 22) - shop keys split into read and write (Task 23) - Kustomize base, components, overlay encry, CI validation (Task 26) Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), reviewed tasks: - brood frames, read endpoints, apiary overview and offline pack - network basics, offline queue v3, navigation sheets, form building blocks - new inspection with last-time hints and weather, varroa per day, treatment prefill, apiary page with round - inspection photo (Task 10) is implemented, review pending Not rolled out. Remaining tasks follow in later commits. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
10cbe9bf2d
commit
c01a38835a
340 files changed
+25695
-1808
No files matched your search
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describeWeatherCode, weatherPrefillFor } from "./describe";
|
||||
import type { DailyWeather } from "./types";
|
||||
|
||||
function day(overrides: Partial<DailyWeather> = {}): DailyWeather {
|
||||
return {
|
||||
date: "2026-10-13",
|
||||
tempMaxC: 21.54,
|
||||
tempMinC: 9,
|
||||
precipitationMm: 0.4,
|
||||
windSpeedMaxKmh: 12,
|
||||
inspectionSuitable: true,
|
||||
weatherCode: 2,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("describeWeatherCode", () => {
|
||||
it.each([
|
||||
[0, "Sonnig"],
|
||||
[1, "Überwiegend sonnig"],
|
||||
[2, "Teilweise bewölkt"],
|
||||
[3, "Bedeckt"],
|
||||
[45, "Nebel"],
|
||||
[48, "Nebel"],
|
||||
[53, "Nieselregen"],
|
||||
[61, "Regen"],
|
||||
[67, "Regen"],
|
||||
[75, "Schnee"],
|
||||
[81, "Regenschauer"],
|
||||
[86, "Schneeschauer"],
|
||||
[95, "Gewitter"],
|
||||
[99, "Gewitter"],
|
||||
])("describes code %i as %s", (code, text) => {
|
||||
expect(describeWeatherCode(code)).toBe(text);
|
||||
});
|
||||
|
||||
it("returns null for unknown, missing or null codes", () => {
|
||||
expect(describeWeatherCode(4)).toBeNull();
|
||||
expect(describeWeatherCode(null)).toBeNull();
|
||||
expect(describeWeatherCode(undefined)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("weatherPrefillFor", () => {
|
||||
it("builds text and temperature for the chosen date", () => {
|
||||
const prefill = weatherPrefillFor([day({ date: "2026-10-12" }), day()], "2026-10-13");
|
||||
|
||||
expect(prefill).toEqual({
|
||||
weather: "Teilweise bewölkt, 0.4 mm, Wind 12 km/h",
|
||||
temperature: "21.5",
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves out parts that are null", () => {
|
||||
const prefill = weatherPrefillFor(
|
||||
[day({ weatherCode: null, precipitationMm: null })],
|
||||
"2026-10-13",
|
||||
);
|
||||
|
||||
expect(prefill).toEqual({ weather: "Wind 12 km/h", temperature: "21.5" });
|
||||
});
|
||||
|
||||
it("returns null outside the weather window", () => {
|
||||
expect(weatherPrefillFor([day()], "2026-09-01")).toBeNull();
|
||||
expect(weatherPrefillFor([], "2026-10-13")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when the day holds nothing usable", () => {
|
||||
const empty = day({
|
||||
weatherCode: null,
|
||||
precipitationMm: null,
|
||||
windSpeedMaxKmh: null,
|
||||
tempMaxC: null,
|
||||
});
|
||||
|
||||
expect(weatherPrefillFor([empty], "2026-10-13")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { toCalendarDay } from "@/lib/dates";
|
||||
import type { DailyWeather } from "./types";
|
||||
|
||||
/** WMO weather codes in plain German, as the beekeeper would say it. */
|
||||
const WEATHER_CODE_RANGES: readonly { from: number; to: number; text: string }[] = [
|
||||
{ from: 0, to: 0, text: "Sonnig" },
|
||||
{ from: 1, to: 1, text: "Überwiegend sonnig" },
|
||||
{ from: 2, to: 2, text: "Teilweise bewölkt" },
|
||||
{ from: 3, to: 3, text: "Bedeckt" },
|
||||
{ from: 45, to: 48, text: "Nebel" },
|
||||
{ from: 51, to: 57, text: "Nieselregen" },
|
||||
{ from: 61, to: 67, text: "Regen" },
|
||||
{ from: 71, to: 77, text: "Schnee" },
|
||||
{ from: 80, to: 82, text: "Regenschauer" },
|
||||
{ from: 85, to: 86, text: "Schneeschauer" },
|
||||
{ from: 95, to: 99, text: "Gewitter" },
|
||||
];
|
||||
|
||||
/** `null` for a missing or unknown code: nothing is guessed. */
|
||||
export function describeWeatherCode(code: number | null | undefined): string | null {
|
||||
if (code === null || code === undefined) return null;
|
||||
const match = WEATHER_CODE_RANGES.find((range) => code >= range.from && code <= range.to);
|
||||
return match?.text ?? null;
|
||||
}
|
||||
|
||||
export interface WeatherPrefill {
|
||||
weather: string;
|
||||
temperature: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The inspection's weather fields for one day of the apiary's weather data:
|
||||
* the description with precipitation and wind (parts that are unknown are
|
||||
* left out) and the day's maximum temperature with one decimal. `null`
|
||||
* outside the forecast window or when the day holds nothing usable.
|
||||
*/
|
||||
export function weatherPrefillFor(
|
||||
daily: readonly DailyWeather[],
|
||||
isoDate: string,
|
||||
): WeatherPrefill | null {
|
||||
const day = daily.find((entry) => toCalendarDay(entry.date) === isoDate);
|
||||
if (!day) return null;
|
||||
|
||||
const parts = [
|
||||
describeWeatherCode(day.weatherCode),
|
||||
day.precipitationMm != null ? `${day.precipitationMm} mm` : null,
|
||||
day.windSpeedMaxKmh != null ? `Wind ${day.windSpeedMaxKmh} km/h` : null,
|
||||
].filter((part): part is string => part !== null);
|
||||
const weather = parts.join(", ");
|
||||
const temperature = day.tempMaxC != null ? day.tempMaxC.toFixed(1) : "";
|
||||
|
||||
if (weather === "" && temperature === "") return null;
|
||||
return { weather, temperature };
|
||||
}
|
||||
@@ -5,6 +5,8 @@ export interface DailyWeather {
|
||||
precipitationMm: number | null;
|
||||
windSpeedMaxKmh: number | null;
|
||||
inspectionSuitable: boolean | null;
|
||||
/** WMO weather code; null until the backend delivers it. */
|
||||
weatherCode: number | null;
|
||||
}
|
||||
|
||||
export interface ApiaryWeather {
|
||||
|
||||
Reference in new issue
Block a user