diff --git a/Dockerfile b/Dockerfile index 6273d81a6c8..9678f3d9393 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM europe-north1-docker.pkg.dev/cgr-nav/pull-through/nav.no/node:22-slim LABEL org.opencontainers.image.source=https://github.com/navikt/fp-frontend ENV TZ="Europe/Oslo" -ENV NODE_ENV production +ENV NODE_ENV=production WORKDIR /app diff --git a/apps/fp-frontend/src/app/components/Dekorator.tsx b/apps/fp-frontend/src/app/components/Dekorator.tsx index 247f9d4af9a..2545c006c45 100644 --- a/apps/fp-frontend/src/app/components/Dekorator.tsx +++ b/apps/fp-frontend/src/app/components/Dekorator.tsx @@ -19,6 +19,7 @@ import { notEmpty } from '@navikt/fp-utils'; import { initFetchOptions } from '../../data/fagsakApi'; import { JOURNALFØRING_PATH, UTBETALINGSDATA_PATH } from '../paths'; +import { useAppVersjonDeteksjon } from './useAppVersjonDeteksjon'; type QueryStrings = { errorcode?: string; @@ -43,6 +44,7 @@ export const Dekorator = ({ setTheme, }: Props) => { const intl = useIntl(); + const nyVersjonTilgjengelig = useAppVersjonDeteksjon(); const errorMessages = useRestApiError(); const { removeErrorMessages } = useRestApiErrorDispatcher(); @@ -54,6 +56,7 @@ export const Dekorator = ({ const visLos = (e: React.SyntheticEvent) => { if (e.type === 'click') { void navigate('/'); + window.location.reload(); } if (e.type === 'contextmenu') { globalThis.open('/', '_newtab'); @@ -115,6 +118,7 @@ export const Dekorator = ({ eksterneLenker={eksterneLenker} theme={theme} setTheme={setTheme} + nyVersjonTilgjengelig={nyVersjonTilgjengelig} /> ); }; diff --git a/apps/fp-frontend/src/app/components/useAppVersjonDeteksjon.tsx b/apps/fp-frontend/src/app/components/useAppVersjonDeteksjon.tsx new file mode 100644 index 00000000000..e87f46862c2 --- /dev/null +++ b/apps/fp-frontend/src/app/components/useAppVersjonDeteksjon.tsx @@ -0,0 +1,44 @@ +import { useEffect, useState } from 'react'; + +const POLLING_INTERVAL_MS = 10 * 60 * 1000; + +export const useAppVersjonDeteksjon = () => { + const [serverVersjon, setServerVersjon] = useState(null); + const [nyVersjonTilgjengelig, setNyVersjonTilgjengelig] = useState(false); + + const [klientVersjon, setKlientVersjon] = useState(null); + + useEffect(() => { + const sjekkServerVersjon = async () => { + try { + const res = await fetch('/version'); + + if (res.ok) { + const data = (await res.json()) as { app_image?: string }; + if (data.app_image) { + if (klientVersjon === null) { + setKlientVersjon(data.app_image); + } + + if (data.app_image !== serverVersjon) { + setServerVersjon(data.app_image); + + if (klientVersjon && data.app_image !== klientVersjon) { + setNyVersjonTilgjengelig(true); + } + } + } + } + } catch (e) { + // eslint-disable-next-line no-console + console.error('Feilet med å hente app-versjon:', e); + } + }; + void sjekkServerVersjon(); + + const interval = setInterval(sjekkServerVersjon, POLLING_INTERVAL_MS); + return () => clearInterval(interval); + }, [klientVersjon, serverVersjon]); + + return nyVersjonTilgjengelig; +}; diff --git a/packages/sak/dekorator/i18n/nb_NO.json b/packages/sak/dekorator/i18n/nb_NO.json index c88b6b09789..acd592d38c6 100644 --- a/packages/sak/dekorator/i18n/nb_NO.json +++ b/packages/sak/dekorator/i18n/nb_NO.json @@ -7,5 +7,6 @@ "DekoratorMedFeilviserSakIndex.Vedtakslosningen": "Vedtaksløsningen", "DekoratorMedFeilviserSakIndex.EksternLenke": "Ekstern lenke", "DekoratorMedFeilviserSakIndex.EndreTilLysTema": "Endre til lyst tema", - "DekoratorMedFeilviserSakIndex.EndreTilMorkTema": "Endre til mørkt tema" + "DekoratorMedFeilviserSakIndex.EndreTilMorkTema": "Endre til mørkt tema", + "DekoratorMedFeilviserSakIndex.NyVersjon": "Det finnes en ny versjon av applikasjonen. Klikk for å laste siden på nytt." } diff --git a/packages/sak/dekorator/src/DekoratorMedFeilviserSakIndex.stories.tsx b/packages/sak/dekorator/src/DekoratorMedFeilviserSakIndex.stories.tsx index cf2f0fae822..681e530b621 100644 --- a/packages/sak/dekorator/src/DekoratorMedFeilviserSakIndex.stories.tsx +++ b/packages/sak/dekorator/src/DekoratorMedFeilviserSakIndex.stories.tsx @@ -25,6 +25,7 @@ const meta = { ], theme: 'light', setTheme: action('setTheme'), + nyVersjonTilgjengelig: false, }, render: storyArgs => { const [args, setArgs] = useState(storyArgs); @@ -83,3 +84,10 @@ export const MedFeilmeldingDetaljer: Story = { ], }, }; + +export const NyAppVersjonErDetektert: Story = { + args: { + feilmeldinger: [], + nyVersjonTilgjengelig: true, + }, +}; diff --git a/packages/sak/dekorator/src/DekoratorMedFeilviserSakIndex.tsx b/packages/sak/dekorator/src/DekoratorMedFeilviserSakIndex.tsx index 7cd819f5e52..2bf717c6932 100644 --- a/packages/sak/dekorator/src/DekoratorMedFeilviserSakIndex.tsx +++ b/packages/sak/dekorator/src/DekoratorMedFeilviserSakIndex.tsx @@ -1,8 +1,8 @@ import React, { type ComponentProps, useEffect, useRef } from 'react'; import { FormattedMessage, RawIntlProvider } from 'react-intl'; -import { ExternalLinkIcon, MenuGridIcon, MoonIcon, SunIcon } from '@navikt/aksel-icons'; -import { Dropdown, InternalHeader, Link, Spacer, Theme } from '@navikt/ds-react'; +import { ArrowCirclepathIcon, ExternalLinkIcon, MenuGridIcon, MoonIcon, SunIcon } from '@navikt/aksel-icons'; +import { Dropdown, InternalHeader, Link, Spacer, Theme, Tooltip } from '@navikt/ds-react'; import { createIntl } from '@navikt/ft-utils'; import { FeilmeldingPanel } from './components/FeilmeldingPanel'; @@ -27,6 +27,7 @@ interface Props { eksterneLenker: DekoratorLenke[]; theme: ComponentProps['theme']; setTheme: (theme: NonNullable['theme']>) => void; + nyVersjonTilgjengelig?: boolean; } /** @@ -48,6 +49,7 @@ export const DekoratorMedFeilviserSakIndex = ({ eksterneLenker, theme, setTheme, + nyVersjonTilgjengelig, }: Props) => { const fixedHeaderRef = useRef(null); useEffect(() => { @@ -64,10 +66,17 @@ export const DekoratorMedFeilviserSakIndex = ({ {tittel} + {nyVersjonTilgjengelig && ( + window.location.reload()}> + + + + + )} diff --git a/server/src/server.ts b/server/src/server.ts index 8495976e5c6..7ea30632ed2 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -78,6 +78,19 @@ function startApp() { // The routes below require the user to be authenticated server.use(verifyToken); + server.get(["/version"], async (_, res, next) => { + try { + res.set("Cache-Control", "no-store"); + res + .json({ + app_image: process.env.NAIS_APP_IMAGE, + }) + .send(); + } catch (error) { + return next(error); + } + }); + server.get(["/logout"], async (req, res) => { if (req.headers.authorization) { res.redirect("/oauth2/logout");