From 36224026b45d3d1e50e2c4446fd40ead63fd256f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 30 Sep 2025 16:09:42 +0200 Subject: [PATCH 01/13] Add cache storage and use it to the farm and field norms route --- .../farm.$b_id_farm.$calendar.norms.tsx | 92 ++++++++++++++++++- fdm-app/app/store/calculation-cache.ts | 23 +++++ fdm-app/package.json | 2 + pnpm-lock.yaml | 17 ++++ 4 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 fdm-app/app/store/calculation-cache.ts diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx index fd3f464de..a85c40477 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx @@ -4,12 +4,14 @@ import { type GebruiksnormResult, } from "@svenvw/fdm-calculator" import { getFarm, getFarms, getFields } from "@svenvw/fdm-core" -import { Suspense, use } from "react" +import hash from "object-hash" +import { Suspense, use, useEffect } from "react" import { data, type LoaderFunctionArgs, type MetaFunction, NavLink, + redirect, useLoaderData, useLocation, } from "react-router" @@ -30,7 +32,9 @@ import { getCalendar, getTimeframe } from "~/lib/calendar" import { clientConfig } from "~/lib/config" import { handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" +import { useFarmNormsCache } from "~/store/calculation-cache" import { useFieldFilterStore } from "~/store/field-filter" +import type { Route } from "./+types/farm.$b_id_farm.$calendar.norms" interface FieldNorm { b_id: string @@ -119,19 +123,45 @@ export async function loader({ request, params }: LoaderFunctionArgs) { let errorMessage = null as string | null let hasFieldNormErrors = false const fieldErrorMessages: string[] = [] + + const url = new URL(request.url) + const cacheHash = url.searchParams.get("cacheHash") + + let inputHash: string | undefined try { // Calculate norms per field const functionsForms = createFunctionsForNorms("NL", calendar) - const fieldNormPromises = fields.map(async (field) => { + const inputPromises = fields.map(async (field) => { try { // Collect the input - const input = await functionsForms.collectInputForNorms( + return await functionsForms.collectInputForNorms( fdm, session.principal_id, field.b_id, ) + } catch (error) { + return { + b_id: field.b_id, + b_area: field.b_area, + errorMessage: String(error).replace("Error: ", ""), + } + } + }) + const inputs = await Promise.all(inputPromises) + inputHash = hash(inputs) + if (inputHash === cacheHash) { + return { useCache: true } + } + + const fieldNormPromises = inputs.map(async (input) => { + const { field } = input + if (input.errorMessage) { + return input + } + + try { // Calculate the norms const [normManure, normPhosphate, normNitrogen] = await Promise.all([ @@ -219,6 +249,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Return user information from loader return { + inputHash: inputHash, errorMessage: errorMessage, fieldNorms: fieldNorms, farmNorms: farmNorms, @@ -241,6 +272,39 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +const clientCacheMiddleware: Route.ClientMiddlewareFunction = async ( + { params, request }, + next, +) => { + if (typeof window === "undefined") return next() + const requestUrl = new URL(request.url) + + const previousCacheHash = requestUrl.searchParams.get("cacheHash") + let newCacheHash: string | null = previousCacheHash + + // Get cache hash for the cache we (possibly) have + const cachedData = useFarmNormsCache.getState().get(params.b_id_farm) + if (cachedData?.inputHash) { + newCacheHash = cachedData.inputHash + } else { + newCacheHash = null + } + + // Redirect if the `cacheHash` search param was wrong + if (previousCacheHash !== newCacheHash) { + newCacheHash + ? requestUrl.searchParams.set("cacheHash", newCacheHash) + : requestUrl.searchParams.delete("cacheHash") + throw redirect(requestUrl.toString()) + } + + return next() +} + +export const clientMiddleware: Route.ClientMiddlewareFunction[] = [ + clientCacheMiddleware, +] + export default function FarmNormsBlock() { const loaderData = useLoaderData() @@ -281,13 +345,33 @@ export default function FarmNormsBlock() { * would not render until `asyncData` resolves and the fallback would never be shown. */ function Norms(loaderData: Awaited>) { + const data = use(loaderData.asyncData) + + const farmNormsCache = useFarmNormsCache() + + const cachedData = farmNormsCache.get(loaderData.b_id_farm) + + useEffect(() => { + if ( + (!data.useCache || !cachedData?.inputHash) && + !data.errorMessage && + data.inputHash + ) { + farmNormsCache.set(loaderData.b_id_farm, data) + } + }, [loaderData.b_id_farm, data, cachedData?.inputHash, farmNormsCache.set]) + const { farmNorms, fieldNorms, errorMessage, hasFieldNormErrors, fieldErrorMessages, - } = use(loaderData.asyncData) + } = + data.useCache && cachedData + ? farmNormsCache.get(loaderData.b_id_farm) + : data + const { showProductiveOnly } = useFieldFilterStore() const location = useLocation() diff --git a/fdm-app/app/store/calculation-cache.ts b/fdm-app/app/store/calculation-cache.ts new file mode 100644 index 000000000..e6454bbf0 --- /dev/null +++ b/fdm-app/app/store/calculation-cache.ts @@ -0,0 +1,23 @@ +import { create } from "zustand" +import { persist } from "zustand/middleware" + +interface CacheStore { + db: Record + get: (id: string) => T | undefined + set: (id: string, val: T) => void +} + +function createCache(name: string) { + return create( + persist>( + (_set, _get) => ({ + db: {}, + get: (id) => _get().db[id], + set: (id, val) => _set({ db: { ..._get().db, [id]: val } }), + }), + { name }, + ), + ) +} + +export const useFarmNormsCache = createCache("farm-norms-cache") diff --git a/fdm-app/package.json b/fdm-app/package.json index f4659861a..6cb09f518 100644 --- a/fdm-app/package.json +++ b/fdm-app/package.json @@ -52,6 +52,7 @@ "lucide-react": "^0.544.0", "mapbox-gl": "^3.15.0", "next-themes": "^0.4.6", + "object-hash": "^3.0.0", "postgres": "^3.4.7", "posthog-js": "^1.266.0", "posthog-node": "^5.8.4", @@ -93,6 +94,7 @@ "@types/mapbox-gl": "^3.4.1", "@types/mapbox__geojson-extent": "^1.0.3", "@types/mapbox__mapbox-gl-geocoder": "^5.0.0", + "@types/object-hash": "^3.0.6", "@types/react": "^19.1.13", "@types/react-dom": "^19.1.9", "@types/react-map-gl": "^6.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bdeee45ae..15d6924af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -193,6 +193,9 @@ importers: next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + object-hash: + specifier: ^3.0.0 + version: 3.0.0 postgres: specifier: ^3.4.7 version: 3.4.7 @@ -302,6 +305,9 @@ importers: '@types/mapbox__mapbox-gl-geocoder': specifier: ^5.0.0 version: 5.0.0 + '@types/object-hash': + specifier: ^3.0.6 + version: 3.0.6 '@types/react': specifier: ^19.1.13 version: 19.1.13 @@ -4818,6 +4824,9 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/object-hash@3.0.6': + resolution: {integrity: sha512-fOBV8C1FIu2ELinoILQ+ApxcUKz4ngq+IWUYrxSGjXzzjUALijilampwkMgEtJ+h2njAW3pi853QpzNVCHB73w==} + '@types/pbf@3.0.5': resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==} @@ -8062,6 +8071,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -16646,6 +16659,8 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/object-hash@3.0.6': {} + '@types/pbf@3.0.5': {} '@types/pg-pool@2.0.6': @@ -20411,6 +20426,8 @@ snapshots: object-assign@4.1.1: {} + object-hash@3.0.0: {} + object-inspect@1.13.4: {} object-keys@1.1.1: {} From 8dde600e7c81d7ea72c16fece4f544484aec382f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 30 Sep 2025 16:18:10 +0200 Subject: [PATCH 02/13] Add changeset --- .changeset/rotten-rocks-argue.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rotten-rocks-argue.md diff --git a/.changeset/rotten-rocks-argue.md b/.changeset/rotten-rocks-argue.md new file mode 100644 index 000000000..8d8bda2f3 --- /dev/null +++ b/.changeset/rotten-rocks-argue.md @@ -0,0 +1,5 @@ +--- +"@svenvw/fdm-app": minor +--- + +Now some application calculation results are cached in the browser local storage. The server only performs a cached calculation again if the cache's input hash doesn't match the current input's hash. From 0d6554b8748926e62ff4a0d80d79ddad46beacae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Fri, 3 Oct 2025 15:04:04 +0200 Subject: [PATCH 03/13] Add nitrogen balance calculation cache --- .../app/components/blocks/header/balance.tsx | 6 +- ...rm.$b_id_farm.$calendar.balance._index.tsx | 6 -- ..._farm.$calendar.balance.nitrogen.$b_id.tsx | 31 ++++++- ...farm.$calendar.balance.nitrogen._index.tsx | 41 ++++++++- .../farm.$b_id_farm.$calendar.balance.tsx | 91 +++++++++++++++++++ .../farm.$b_id_farm.$calendar.norms.tsx | 3 + fdm-app/app/store/calculation-cache.ts | 14 ++- 7 files changed, 177 insertions(+), 15 deletions(-) delete mode 100644 fdm-app/app/routes/farm.$b_id_farm.$calendar.balance._index.tsx create mode 100644 fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.tsx diff --git a/fdm-app/app/components/blocks/header/balance.tsx b/fdm-app/app/components/blocks/header/balance.tsx index af469be8d..3735ce783 100644 --- a/fdm-app/app/components/blocks/header/balance.tsx +++ b/fdm-app/app/components/blocks/header/balance.tsx @@ -30,8 +30,10 @@ export function HeaderBalance({ <> - - Nutriƫntenbalans + + + Nutriƫntenbalans + diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance._index.tsx deleted file mode 100644 index 80c839111..000000000 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance._index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { redirect } from "react-router" - -export async function loader() { - // Redirect to nitrogen page - return redirect("./nitrogen") -} diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen.$b_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen.$b_id.tsx index c849e8596..a92c07d1c 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen.$b_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen.$b_id.tsx @@ -11,7 +11,8 @@ import { CircleAlert, CircleCheck, } from "lucide-react" -import { Suspense, use } from "react" +import hash from "object-hash" +import { Suspense, use, useEffect } from "react" import { data, type LoaderFunctionArgs, @@ -38,6 +39,7 @@ import { clientConfig } from "~/lib/config" import { fdm } from "~/lib/fdm.server" import { useCalendarStore } from "~/store/calendar" import { serverConfig } from "../lib/config.server" +import { useFieldNitrogenBalanceCache } from "../store/calculation-cache" // Meta export const meta: MetaFunction = () => { @@ -91,6 +93,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Get details of field const field = await getField(fdm, session.principal_id, b_id) + const url = new URL(request.url) + const cacheHash = url.searchParams.get("cacheHash") + // Return promise directly for React Router v7 Suspense pattern const nitrogenBalancePromise = collectInputForNitrogenBalance( fdm, @@ -100,6 +105,10 @@ export async function loader({ request, params }: LoaderFunctionArgs) { datasetsUrl, ) .then(async (input) => { + const inputHash = hash(input) + if (inputHash === cacheHash) { + return { useCache: true } + } const result = await calculateNitrogenBalance(input) return { input: input.fields.find( @@ -110,6 +119,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { (field: { b_id: string }) => field.b_id === b_id, ), errorMessage: null, + inputHash: inputHash, } }) .catch((error) => ({ @@ -154,12 +164,29 @@ function NitrogenBalance({ field, nitrogenBalanceResult, }: Awaited>) { - const { input, result, errorMessage } = use(nitrogenBalanceResult) + const data = use(nitrogenBalanceResult) const location = useLocation() const page = location.pathname const calendar = useCalendarStore((state) => state.calendar) + const fieldNitrogenBalanceCache = useFieldNitrogenBalanceCache() + + const cachedData = fieldNitrogenBalanceCache.get(farm.b_id_farm) + + useEffect(() => { + if ( + (!data.useCache || !cachedData?.inputHash) && + !data.errorMessage && + data.inputHash + ) { + fieldNitrogenBalanceCache.set(field.b_id, data) + } + }, [field.b_id, data, cachedData?.inputHash, fieldNitrogenBalanceCache.set]) + + const { input, result, errorMessage } = + data.useCache && cachedData ? cachedData : data + if (!input) { return (
diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen._index.tsx index 97c8ed505..2320c7bf5 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen._index.tsx @@ -12,7 +12,8 @@ import { CircleAlert, CircleCheck, } from "lucide-react" -import { Suspense, use } from "react" +import hash from "object-hash" +import { Suspense, use, useEffect } from "react" import { data, type LoaderFunctionArgs, @@ -36,6 +37,8 @@ import { getTimeframe } from "~/lib/calendar" import { clientConfig } from "~/lib/config" import { fdm } from "~/lib/fdm.server" import { useFieldFilterStore } from "~/store/field-filter" +import { useFarmNitrogenBalanceCache } from "../store/calculation-cache" +import type { Route } from "./+types/farm.$b_id_farm.$calendar.balance.nitrogen._index" // Meta export const meta: MetaFunction = () => { @@ -78,6 +81,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // Get details of fields const fields = await getFields(fdm, session.principal_id, b_id_farm) + const url = new URL(request.url) + const cacheHash = url.searchParams.get("cacheHash") + const asyncData = (async () => { // Collect input data for nutrient balance calculation const nitrogenBalanceInput = await collectInputForNitrogenBalance( @@ -87,6 +93,11 @@ export async function loader({ request, params }: LoaderFunctionArgs) { timeframe, ) + const inputHash = hash(nitrogenBalanceInput) + if (inputHash === cacheHash) { + return { useCache: true } + } + let nitrogenBalanceResult = null as NitrogenBalanceNumeric | null let errorMessage = null as string | null try { @@ -99,6 +110,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { return { nitrogenBalanceResult: nitrogenBalanceResult, errorMessage: errorMessage, + inputHash: inputHash, } })() @@ -140,10 +152,33 @@ function FarmBalanceNitrogenOverview({ }: Awaited>) { const location = useLocation() const page = location.pathname - const { nitrogenBalanceResult, errorMessage } = use(asyncData) + const data = use(asyncData) const { showProductiveOnly } = useFieldFilterStore() - const resolvedNitrogenBalanceResult = nitrogenBalanceResult + const farmNitrogenBalanceCache = useFarmNitrogenBalanceCache() + + const cachedData = farmNitrogenBalanceCache.get(farm.b_id_farm) + + useEffect(() => { + if ( + (!data.useCache || !cachedData?.inputHash) && + !data.errorMessage && + data.inputHash + ) { + farmNitrogenBalanceCache.set(farm.b_id_farm, data) + } + }, [ + farm.b_id_farm, + data, + cachedData?.inputHash, + farmNitrogenBalanceCache.set, + ]) + + const { + nitrogenBalanceResult: resolvedNitrogenBalanceResult, + errorMessage, + } = data.useCache && cachedData ? cachedData : data + if (errorMessage) { return (
diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.tsx new file mode 100644 index 000000000..6d0bc3074 --- /dev/null +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.tsx @@ -0,0 +1,91 @@ +import { redirect } from "react-router" +import type { CacheStore, DataWithInputHash } from "../store/calculation-cache" +import { + useFarmNitrogenBalanceCache, + useFieldNitrogenBalanceCache, +} from "../store/calculation-cache" +import type { Route } from "./+types/farm.$b_id_farm.$calendar.balance" + +// In case the user navigated directly by URL +export function loader({ params, request }: Route.LoaderArgs) { + if (/\/balance\/?($|\?)/.test(request.url)) { + throw redirect( + `/farm/${params.b_id_farm}/${params.calendar}/balance/nitrogen`, + ) + } + + return {} +} + +// In case the user navigated within the application +const redirectMiddleware: Route.ClientMiddlewareFunction = ( + { request, params }, + next, +) => { + if (/\/balance\/?($|\?)/.test(request.url)) { + throw redirect( + `/farm/${params.b_id_farm}/${params.calendar}/balance/nitrogen`, + ) + } + + return next() +} + +/** + * Client middleware that redirects with the most recent cacheHash, obtained from the provided cache store, when the route matches the provided matcher, if needed. + * + * @param matcherProvider function that return a regexp that matches url strings like `/farm/b_id_farm/calendar/balance/nitrogen` + * @param storeProvider cache store to use for this match + * @param getId function to oobtain the id out of the client middleware function args + */ +function balanceCacheMiddleware( + matcherProvider: () => RegExp, + storeProvider: () => CacheStore, + getId: (args: Parameters[0]) => string, +): Route.ClientMiddlewareFunction { + return (args, next) => { + const { request } = args + if (typeof window === "undefined") return next() + + const requestUrl = new URL(request.url) + if (!matcherProvider().test(requestUrl.pathname)) return next() + + const previousCacheHash = requestUrl.searchParams.get("cacheHash") + let newCacheHash: string | null = previousCacheHash + + // Get cache hash for the cache we (possibly) have + const cachedData = storeProvider().get(getId(args)) + if (cachedData?.inputHash) { + newCacheHash = cachedData.inputHash + } else { + newCacheHash = null + } + + // Redirect if the `cacheHash` search param was wrong + if (previousCacheHash !== newCacheHash) { + newCacheHash + ? requestUrl.searchParams.set("cacheHash", newCacheHash) + : requestUrl.searchParams.delete("cacheHash") + throw redirect(requestUrl.toString()) + } + + return next() + } +} + +export const clientMiddleware = [ + // Redirect to nitrogen balance if what kind of balance analysis needed is not known yet + redirectMiddleware, + // Farm nitrogen + balanceCacheMiddleware( + () => /\/nitrogen\/?$/, + () => useFarmNitrogenBalanceCache.getState(), + ({ params }) => params.b_id_farm, + ), + // Field nitrogen + balanceCacheMiddleware( + () => /\/nitrogen\/.+\/?$/, + () => useFieldNitrogenBalanceCache.getState(), + ({ params }) => params.b_id || "", + ), +] diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx index a85c40477..a1229ed42 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx @@ -272,6 +272,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +/** + * Client middleware that redirects with the most recent cacheHash search parameter as needed + */ const clientCacheMiddleware: Route.ClientMiddlewareFunction = async ( { params, request }, next, diff --git a/fdm-app/app/store/calculation-cache.ts b/fdm-app/app/store/calculation-cache.ts index e6454bbf0..911206688 100644 --- a/fdm-app/app/store/calculation-cache.ts +++ b/fdm-app/app/store/calculation-cache.ts @@ -1,13 +1,17 @@ import { create } from "zustand" import { persist } from "zustand/middleware" -interface CacheStore { +export interface DataWithInputHash { + inputHash?: string | undefined +} + +export interface CacheStore { db: Record get: (id: string) => T | undefined set: (id: string, val: T) => void } -function createCache(name: string) { +function createCache(name: string) { return create( persist>( (_set, _get) => ({ @@ -21,3 +25,9 @@ function createCache(name: string) { } export const useFarmNormsCache = createCache("farm-norms-cache") +export const useFarmNitrogenBalanceCache = createCache( + "farm-nitrogen-balance-cache", +) +export const useFieldNitrogenBalanceCache = createCache( + "field-nitrogen-balance-cache", +) From 46a9b20382a7458625b86e383593ec89f5bc17cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Fri, 3 Oct 2025 16:37:57 +0200 Subject: [PATCH 04/13] Fix key bug with field nitrogen balance route --- .../routes/farm.$b_id_farm.$calendar.balance.nitrogen.$b_id.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen.$b_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen.$b_id.tsx index a92c07d1c..e9bef9259 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen.$b_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen.$b_id.tsx @@ -172,7 +172,7 @@ function NitrogenBalance({ const fieldNitrogenBalanceCache = useFieldNitrogenBalanceCache() - const cachedData = fieldNitrogenBalanceCache.get(farm.b_id_farm) + const cachedData = fieldNitrogenBalanceCache.get(field.b_id) useEffect(() => { if ( From d9a9e273243ce56c0154dc109882f63c9ddf04ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Fri, 3 Oct 2025 16:39:24 +0200 Subject: [PATCH 05/13] Remove unused import --- .../routes/farm.$b_id_farm.$calendar.balance.nitrogen._index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen._index.tsx index 2320c7bf5..78bafede9 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen._index.tsx @@ -38,7 +38,6 @@ import { clientConfig } from "~/lib/config" import { fdm } from "~/lib/fdm.server" import { useFieldFilterStore } from "~/store/field-filter" import { useFarmNitrogenBalanceCache } from "../store/calculation-cache" -import type { Route } from "./+types/farm.$b_id_farm.$calendar.balance.nitrogen._index" // Meta export const meta: MetaFunction = () => { From cf5a68f2324618ba07c508aae45042f0d0b6ea1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Fri, 3 Oct 2025 16:40:22 +0200 Subject: [PATCH 06/13] Add PUBLIC_FDM_CALCULATOR_VERSION definition and version the cache store --- fdm-app/app/store/calculation-cache.ts | 5 ++++- fdm-app/vite.config.ts | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/fdm-app/app/store/calculation-cache.ts b/fdm-app/app/store/calculation-cache.ts index 911206688..e772b025a 100644 --- a/fdm-app/app/store/calculation-cache.ts +++ b/fdm-app/app/store/calculation-cache.ts @@ -19,7 +19,10 @@ function createCache(name: string) { get: (id) => _get().db[id], set: (id, val) => _set({ db: { ..._get().db, [id]: val } }), }), - { name }, + { + name, + version: `fdm-calculator:${PUBLIC_FDM_CALCULATOR_VERSION}`, + }, ), ) } diff --git a/fdm-app/vite.config.ts b/fdm-app/vite.config.ts index 8b7b5fa19..9ea1230f6 100644 --- a/fdm-app/vite.config.ts +++ b/fdm-app/vite.config.ts @@ -1,3 +1,5 @@ +import fs from "node:fs/promises" + import { reactRouter } from "@react-router/dev/vite" import { type SentryReactRouterBuildOptions, @@ -7,7 +9,16 @@ import tailwindcss from "@tailwindcss/vite" import { defineConfig } from "vite" import tsconfigPaths from "vite-tsconfig-paths" -export default defineConfig((config) => { +export default defineConfig(async (config) => { + // We need to go one directory up since package.json is not inside the dist folder + const fdmCalculatorPackageJsonPath = new URL( + "../package.json", + import.meta.resolve("@svenvw/fdm-calculator"), + ) + const fdmCalculatorPackage = JSON.parse( + await fs.readFile(fdmCalculatorPackageJsonPath, { encoding: "utf-8" }), + ) + return { plugins: [ reactRouter(), @@ -35,6 +46,9 @@ export default defineConfig((config) => { envPrefix: "PUBLIC_", define: { global: {}, + PUBLIC_FDM_CALCULATOR_VERSION: JSON.stringify( + fdmCalculatorPackage.version || "0.7.0", + ), }, ssr: { noExternal: ["posthog-js", "posthog-js/react"], From d08b93f8600f0c95a21476bb8b2561ac16be032f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Mon, 6 Oct 2025 13:11:04 +0200 Subject: [PATCH 07/13] Add caching to the nutrient advice route --- fdm-app/app/lib/middleware.ts | 47 ++++++++++++++++++ .../farm.$b_id_farm.$calendar.balance.tsx | 49 ++----------------- ...d_farm.$calendar.nutrient_advice.$b_id.tsx | 40 ++++++++++++++- ...m.$b_id_farm.$calendar.nutrient_advice.tsx | 41 ++++++++++++++++ fdm-app/app/store/calculation-cache.ts | 1 + 5 files changed, 131 insertions(+), 47 deletions(-) create mode 100644 fdm-app/app/lib/middleware.ts diff --git a/fdm-app/app/lib/middleware.ts b/fdm-app/app/lib/middleware.ts new file mode 100644 index 000000000..f9204832c --- /dev/null +++ b/fdm-app/app/lib/middleware.ts @@ -0,0 +1,47 @@ +import { redirect } from "react-router" +import type { Route } from "../+types/root" +import type { CacheStore, DataWithInputHash } from "../store/calculation-cache" + +/** + * Client middleware that redirects with the most recent cacheHash, obtained from the provided cache store, when the route matches the provided matcher, if needed. + * + * @param matcherProvider function that return a regexp that matches url strings like `/farm/b_id_farm/calendar/balance/nitrogen` + * @param storeProvider cache store to use for this match + * @param getId function to oobtain the id out of the client middleware function args + * + * @returns a client middleware function that either throws redirect or calls next as needed + */ +export function splatCacheMiddleware( + matcherProvider: () => RegExp, + storeProvider: () => CacheStore, + getId: (args: Parameters[0]) => string, +): Route.ClientMiddlewareFunction { + return (args, next) => { + const { request } = args + if (typeof window === "undefined") return next() + + const requestUrl = new URL(request.url) + if (!matcherProvider().test(requestUrl.pathname)) return next() + + const previousCacheHash = requestUrl.searchParams.get("cacheHash") + let newCacheHash: string | null = previousCacheHash + + // Get cache hash for the cache we (possibly) have + const cachedData = storeProvider().get(getId(args)) + if (cachedData?.inputHash) { + newCacheHash = cachedData.inputHash + } else { + newCacheHash = null + } + + // Redirect if the `cacheHash` search param was wrong + if (previousCacheHash !== newCacheHash) { + newCacheHash + ? requestUrl.searchParams.set("cacheHash", newCacheHash) + : requestUrl.searchParams.delete("cacheHash") + throw redirect(requestUrl.toString()) + } + + return next() + } +} diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.tsx index 6d0bc3074..ed59bb5bb 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.tsx @@ -5,6 +5,7 @@ import { useFieldNitrogenBalanceCache, } from "../store/calculation-cache" import type { Route } from "./+types/farm.$b_id_farm.$calendar.balance" +import { splatCacheMiddleware } from "../lib/middleware" // In case the user navigated directly by URL export function loader({ params, request }: Route.LoaderArgs) { @@ -31,59 +32,17 @@ const redirectMiddleware: Route.ClientMiddlewareFunction = ( return next() } -/** - * Client middleware that redirects with the most recent cacheHash, obtained from the provided cache store, when the route matches the provided matcher, if needed. - * - * @param matcherProvider function that return a regexp that matches url strings like `/farm/b_id_farm/calendar/balance/nitrogen` - * @param storeProvider cache store to use for this match - * @param getId function to oobtain the id out of the client middleware function args - */ -function balanceCacheMiddleware( - matcherProvider: () => RegExp, - storeProvider: () => CacheStore, - getId: (args: Parameters[0]) => string, -): Route.ClientMiddlewareFunction { - return (args, next) => { - const { request } = args - if (typeof window === "undefined") return next() - - const requestUrl = new URL(request.url) - if (!matcherProvider().test(requestUrl.pathname)) return next() - - const previousCacheHash = requestUrl.searchParams.get("cacheHash") - let newCacheHash: string | null = previousCacheHash - - // Get cache hash for the cache we (possibly) have - const cachedData = storeProvider().get(getId(args)) - if (cachedData?.inputHash) { - newCacheHash = cachedData.inputHash - } else { - newCacheHash = null - } - - // Redirect if the `cacheHash` search param was wrong - if (previousCacheHash !== newCacheHash) { - newCacheHash - ? requestUrl.searchParams.set("cacheHash", newCacheHash) - : requestUrl.searchParams.delete("cacheHash") - throw redirect(requestUrl.toString()) - } - - return next() - } -} - export const clientMiddleware = [ // Redirect to nitrogen balance if what kind of balance analysis needed is not known yet redirectMiddleware, // Farm nitrogen - balanceCacheMiddleware( + splatCacheMiddleware( () => /\/nitrogen\/?$/, () => useFarmNitrogenBalanceCache.getState(), - ({ params }) => params.b_id_farm, + ({ params }) => params.b_id_farm || "", ), // Field nitrogen - balanceCacheMiddleware( + splatCacheMiddleware( () => /\/nitrogen\/.+\/?$/, () => useFieldNitrogenBalanceCache.getState(), ({ params }) => params.b_id || "", diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.$b_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.$b_id.tsx index ef0605278..293b3ba4b 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.$b_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.$b_id.tsx @@ -6,7 +6,8 @@ import { getFertilizers, getField, } from "@svenvw/fdm-core" -import { Suspense, use } from "react" +import hash from "object-hash" +import { Suspense, use, useEffect } from "react" import { type LoaderFunctionArgs, type MetaFunction, @@ -28,6 +29,7 @@ import { getCalendar, getTimeframe } from "~/lib/calendar" import { clientConfig } from "~/lib/config" import { handleLoaderError } from "~/lib/error" import { fdm } from "~/lib/fdm.server" +import { useFieldNutrientAdviceCache } from "../store/calculation-cache" // Meta export const meta: MetaFunction = () => { @@ -75,6 +77,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const field = await getField(fdm, session.principal_id, b_id) + const url = new URL(request.url) + const cacheHash = url.searchParams.get("cacheHash") const asyncData = (async () => { try { const currentSoilData = getCurrentSoilData( @@ -120,6 +124,16 @@ export async function loader({ request, params }: LoaderFunctionArgs) { // For now take the first cultivation const b_lu_catalogue = cultivations[0].b_lu_catalogue + const inputHash = hash([ + resolvedCurrentSoilData, + resolvedFertilizerApplications, + resolvedFertilizers, + b_lu_catalogue, + ]) + if (inputHash === cacheHash) { + return { useCache: true } + } + const doses = calculateDose({ applications: resolvedFertilizerApplications, fertilizers: resolvedFertilizers, @@ -138,6 +152,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { fertilizerApplications: resolvedFertilizerApplications, fertilizers: resolvedFertilizers, errorMessage: undefined, + inputHash: inputHash, } } catch (error) { return { errorMessage: String(error).replace("Error: ", "") } @@ -211,9 +226,30 @@ function FieldNutrientAdvice({ traceNutrients: NutrientDescription[] }) { const { field, calendar, nutrientsDescription } = loaderData - const asyncData = use(loaderData.asyncData) + const serverAsyncData = use(loaderData.asyncData) const location = useLocation() + const fieldNutrientAdviceCache = useFieldNutrientAdviceCache() + const cachedData = fieldNutrientAdviceCache.get(field.b_id) + useEffect(() => { + if ( + (!serverAsyncData.useCache || !cachedData?.inputHash) && + !serverAsyncData.errorMessage && + serverAsyncData.inputHash + ) { + fieldNutrientAdviceCache.set(field.b_id, asyncData) + } + }, [ + field.b_id, + serverAsyncData, + cachedData?.inputHash, + fieldNutrientAdviceCache.set, + ]) + + const asyncData = ( + serverAsyncData.useCache ? cachedData : serverAsyncData + ) as typeof serverAsyncData + if (typeof asyncData.errorMessage === "string") { return ( { @@ -118,6 +122,43 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } } +// In case the user navigated within the application +const redirectMiddleware: Route.ClientMiddlewareFunction = ( + { request, params }, + next, +) => { + const url = new URL(request.url) + + if (/\/nutrient_advice\/?$/.test(url.pathname)) { + const nutrientAdviceCache = useFieldNutrientAdviceCache.getState() + + const cachedFieldId = Object.keys(nutrientAdviceCache.db)[0] + + if (cachedFieldId) { + const cachedData = nutrientAdviceCache.get(cachedFieldId) + + if (cachedData?.inputHash) { + throw redirect( + `/farm/${params.b_id_farm}/${params.calendar}/nutrient_advice/${cachedFieldId}?cacheHash=${cachedData.inputHash}`, + ) + } + } + } + + return next() +} + +export const clientMiddleware = [ + // Redirect to nitrogen balance if what kind of balance analysis needed is not known yet + redirectMiddleware, + // Farm nitrogen + splatCacheMiddleware( + () => /\/nutrient_advice\/.+\/?$/, + () => useFieldNutrientAdviceCache.getState(), + ({ params }) => params.b_id || "", + ), +] + /** * Renders the layout for managing farm settings. * diff --git a/fdm-app/app/store/calculation-cache.ts b/fdm-app/app/store/calculation-cache.ts index e772b025a..5920b0bd6 100644 --- a/fdm-app/app/store/calculation-cache.ts +++ b/fdm-app/app/store/calculation-cache.ts @@ -34,3 +34,4 @@ export const useFarmNitrogenBalanceCache = createCache( export const useFieldNitrogenBalanceCache = createCache( "field-nitrogen-balance-cache", ) +export const useFieldNutrientAdviceCache = createCache("field-norms-cache") From 7c0270732bd9555742232ebd7a1ac03751862028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Mon, 6 Oct 2025 14:57:45 +0200 Subject: [PATCH 08/13] Add guard against directly navigating to a calculator url with an invalid cache hash --- ...$b_id_farm.$calendar.balance.nitrogen.$b_id.tsx | 10 ++++++++++ ...b_id_farm.$calendar.balance.nitrogen._index.tsx | 10 ++++++++++ .../routes/farm.$b_id_farm.$calendar.balance.tsx | 5 ++--- .../app/routes/farm.$b_id_farm.$calendar.norms.tsx | 10 ++++++++++ ....$b_id_farm.$calendar.nutrient_advice.$b_id.tsx | 14 ++++++++++++++ 5 files changed, 46 insertions(+), 3 deletions(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen.$b_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen.$b_id.tsx index e9bef9259..90d4d9136 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen.$b_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen.$b_id.tsx @@ -20,6 +20,7 @@ import { NavLink, useLoaderData, useLocation, + useSearchParams, } from "react-router" import { NitrogenBalanceChart } from "~/components/blocks/balance/nitrogen-chart" import NitrogenBalanceDetails from "~/components/blocks/balance/nitrogen-details" @@ -167,6 +168,7 @@ function NitrogenBalance({ const data = use(nitrogenBalanceResult) const location = useLocation() + const [searchParams, setSearchParams] = useSearchParams() const page = location.pathname const calendar = useCalendarStore((state) => state.calendar) @@ -184,6 +186,14 @@ function NitrogenBalance({ } }, [field.b_id, data, cachedData?.inputHash, fieldNitrogenBalanceCache.set]) + if (data.useCache && !cachedData && searchParams.get("cacheHash")) { + setSearchParams((searchParams) => { + searchParams.delete("cacheHash") + return searchParams + }) + return null + } + const { input, result, errorMessage } = data.useCache && cachedData ? cachedData : data diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen._index.tsx index 78bafede9..72191d79b 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen._index.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen._index.tsx @@ -21,6 +21,7 @@ import { NavLink, useLoaderData, useLocation, + useSearchParams, } from "react-router" import { NitrogenBalanceChart } from "~/components/blocks/balance/nitrogen-chart" import { NitrogenBalanceFallback } from "~/components/blocks/balance/skeletons" @@ -151,6 +152,7 @@ function FarmBalanceNitrogenOverview({ }: Awaited>) { const location = useLocation() const page = location.pathname + const [searchParams, setSearchParams] = useSearchParams() const data = use(asyncData) const { showProductiveOnly } = useFieldFilterStore() @@ -173,6 +175,14 @@ function FarmBalanceNitrogenOverview({ farmNitrogenBalanceCache.set, ]) + if (data.useCache && !cachedData && searchParams.get("cacheHash")) { + setSearchParams((searchParams) => { + searchParams.delete("cacheHash") + return searchParams + }) + return null + } + const { nitrogenBalanceResult: resolvedNitrogenBalanceResult, errorMessage, diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.tsx index ed59bb5bb..20e139081 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.tsx @@ -1,11 +1,10 @@ import { redirect } from "react-router" -import type { CacheStore, DataWithInputHash } from "../store/calculation-cache" +import { splatCacheMiddleware } from "~/lib/middleware" import { useFarmNitrogenBalanceCache, useFieldNitrogenBalanceCache, -} from "../store/calculation-cache" +} from "~/store/calculation-cache" import type { Route } from "./+types/farm.$b_id_farm.$calendar.balance" -import { splatCacheMiddleware } from "../lib/middleware" // In case the user navigated directly by URL export function loader({ params, request }: Route.LoaderArgs) { diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx index a1229ed42..ce75aed01 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx @@ -14,6 +14,7 @@ import { redirect, useLoaderData, useLocation, + useSearchParams, } from "react-router" import { FarmTitle } from "~/components/blocks/farm/farm-title" import { Header } from "~/components/blocks/header/base" @@ -348,6 +349,7 @@ export default function FarmNormsBlock() { * would not render until `asyncData` resolves and the fallback would never be shown. */ function Norms(loaderData: Awaited>) { + const [searchParams, setSearchParams] = useSearchParams() const data = use(loaderData.asyncData) const farmNormsCache = useFarmNormsCache() @@ -364,6 +366,14 @@ function Norms(loaderData: Awaited>) { } }, [loaderData.b_id_farm, data, cachedData?.inputHash, farmNormsCache.set]) + if (data.useCache && !cachedData && searchParams.get("cacheHash")) { + setSearchParams((searchParams) => { + searchParams.delete("cacheHash") + return searchParams + }) + return null + } + const { farmNorms, fieldNorms, diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.$b_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.$b_id.tsx index 293b3ba4b..66b790d3b 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.$b_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.$b_id.tsx @@ -13,6 +13,7 @@ import { type MetaFunction, useLoaderData, useLocation, + useSearchParams, } from "react-router" import { FieldNutrientAdviceLayout } from "~/components/blocks/nutrient-advice/layout" import { getNutrientsDescription } from "~/components/blocks/nutrient-advice/nutrients" @@ -228,6 +229,7 @@ function FieldNutrientAdvice({ const { field, calendar, nutrientsDescription } = loaderData const serverAsyncData = use(loaderData.asyncData) const location = useLocation() + const [searchParams, setSearchParams] = useSearchParams() const fieldNutrientAdviceCache = useFieldNutrientAdviceCache() const cachedData = fieldNutrientAdviceCache.get(field.b_id) @@ -246,6 +248,18 @@ function FieldNutrientAdvice({ fieldNutrientAdviceCache.set, ]) + if ( + serverAsyncData.useCache && + !cachedData && + searchParams.get("cacheHash") + ) { + setSearchParams((searchParams) => { + searchParams.delete("cacheHash") + return searchParams + }) + return null + } + const asyncData = ( serverAsyncData.useCache ? cachedData : serverAsyncData ) as typeof serverAsyncData From 03f71abcef3285f5ec0ea38483f4a700d6a12f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Mon, 6 Oct 2025 15:21:01 +0200 Subject: [PATCH 09/13] Fix typo thus fix the potential early use of variable error --- .../routes/farm.$b_id_farm.$calendar.nutrient_advice.$b_id.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.$b_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.$b_id.tsx index 66b790d3b..d95059743 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.$b_id.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.$b_id.tsx @@ -239,7 +239,7 @@ function FieldNutrientAdvice({ !serverAsyncData.errorMessage && serverAsyncData.inputHash ) { - fieldNutrientAdviceCache.set(field.b_id, asyncData) + fieldNutrientAdviceCache.set(field.b_id, serverAsyncData) } }, [ field.b_id, From a4f540e7d234e26889b1e53e30c2356b87c8617d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Mon, 6 Oct 2025 15:24:00 +0200 Subject: [PATCH 10/13] Use NavLink in nutrient advice header --- .../app/components/blocks/header/nutrient-advice.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/fdm-app/app/components/blocks/header/nutrient-advice.tsx b/fdm-app/app/components/blocks/header/nutrient-advice.tsx index 6bd2423f7..d4078da14 100644 --- a/fdm-app/app/components/blocks/header/nutrient-advice.tsx +++ b/fdm-app/app/components/blocks/header/nutrient-advice.tsx @@ -30,10 +30,12 @@ export function HeaderNutrientAdvice({ <> - - Bemestingsadvies + + + Bemestingsadvies + {b_id ? ( From 164ad1030bdd20405ffc9dc57c753a98b17b0bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Mon, 6 Oct 2025 15:37:29 +0200 Subject: [PATCH 11/13] Destructure after checking for error --- fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx index ce75aed01..6f7dbd37b 100644 --- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx +++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx @@ -157,10 +157,10 @@ export async function loader({ request, params }: LoaderFunctionArgs) { } const fieldNormPromises = inputs.map(async (input) => { - const { field } = input if (input.errorMessage) { return input } + const { field } = input try { // Calculate the norms From cee6246b53aff0182160cef188ffd2a1eb5c69cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Mon, 13 Oct 2025 12:34:18 +0200 Subject: [PATCH 12/13] Stabilize nutrient advice field selection --- fdm-app/app/components/blocks/header/nutrient-advice.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/fdm-app/app/components/blocks/header/nutrient-advice.tsx b/fdm-app/app/components/blocks/header/nutrient-advice.tsx index d4078da14..1d972a8bc 100644 --- a/fdm-app/app/components/blocks/header/nutrient-advice.tsx +++ b/fdm-app/app/components/blocks/header/nutrient-advice.tsx @@ -30,12 +30,8 @@ export function HeaderNutrientAdvice({ <> - - - Bemestingsadvies - + typeof window !== "undefined" && window.location.reload()}> + Bemestingsadvies {b_id ? ( From 59832c152c0738c1682ba25ebb24210adf4fa871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Mon, 13 Oct 2025 13:22:44 +0200 Subject: [PATCH 13/13] Use reloadDocument --- fdm-app/app/components/blocks/header/nutrient-advice.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/fdm-app/app/components/blocks/header/nutrient-advice.tsx b/fdm-app/app/components/blocks/header/nutrient-advice.tsx index 1d972a8bc..3336d5e78 100644 --- a/fdm-app/app/components/blocks/header/nutrient-advice.tsx +++ b/fdm-app/app/components/blocks/header/nutrient-advice.tsx @@ -30,8 +30,13 @@ export function HeaderNutrientAdvice({ <> - typeof window !== "undefined" && window.location.reload()}> - Bemestingsadvies + + + Bemestingsadvies + {b_id ? (