From 93f2f5a419a3ef10e471c7ffaf5b7c90324c4bad Mon Sep 17 00:00:00 2001 From: Yasin Date: Mon, 21 Sep 2026 13:09:46 +0200 Subject: [PATCH 1/2] fix(training): keep upcoming events out of the past list The past events query sends include_expired=true, which adds ended events to the upcoming ones rather than returning ended events only. Combined with sort=new, which orders by creation date, anything recently added to TeSS lands at the top of "Past training events", so the two events created today would have been listed as past on the next build. I sort by start date now and drop whatever has not ended yet, fetching twice the page size so ten remain once those are gone. --- src/lib/tess.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/lib/tess.ts b/src/lib/tess.ts index 81df960..069f735 100644 --- a/src/lib/tess.ts +++ b/src/lib/tess.ts @@ -81,14 +81,21 @@ async function fetchList(path: string, params: Params): Promise export const upcomingEvents = (pageSize = 5) => fetchList("events", { page_size: pageSize, country: ["Norway"] }); -export const pastEvents = (pageSize = 10) => - fetchList("events", { - page_size: pageSize, - sort: "new", +const hasEnded = (event: TessEvent) => Date.parse(event.end ?? event.start ?? "") < Date.now(); + +// include_expired adds ended events to the upcoming ones rather than replacing +// them, so the ones still to come are filtered out here; fetching double keeps +// the list full once they are gone. "late" sorts by start date, newest first. +export async function pastEvents(pageSize = 10): Promise> { + const result = await fetchList("events", { + page_size: pageSize * 2, + sort: "late", country: ["Norway"], include_expired: true, include_disabled: false, }); + return { ...result, items: result.items.filter(hasEnded).slice(0, pageSize) }; +} export const materials = (pageSize = 10) => fetchList("materials", { page_size: pageSize, node: ["Norway"] }); From c41d8fb7f7a6d6cebdb133fdf5c14a24fbc8fd3b Mon Sep 17 00:00:00 2001 From: Yasin Date: Mon, 21 Sep 2026 13:09:52 +0200 Subject: [PATCH 2/2] fix(training): load TeSS listings in the browser The listings were fetched during the build, so the page only ever showed what TeSS had on the day of the last deploy. That was Aug 24, and both events added to TeSS today were missing from elixir.no while a local build showed them, which is what #335 reports. Each section is now a React island that asks TeSS when the page loads. It shows a skeleton while waiting and, if TeSS never answers, a link to browse the listing there instead. The request sends Accept: application/json and nothing else, which keeps it a simple CORS request; TeSS rejects the preflight with a 403, so any extra header would break it. With someone waiting on the page, the retry budget goes from 5 x 20s to 3 x 15s, and the build no longer depends on TeSS being up at all. Fixes #335 --- src/components/tess-listings.tsx | 136 ++++++++++++++++++++++++++ src/components/tess-unavailable.astro | 21 ---- src/lib/tess.ts | 31 +++--- src/pages/training/index.astro | 119 ++-------------------- 4 files changed, 163 insertions(+), 144 deletions(-) create mode 100644 src/components/tess-listings.tsx delete mode 100644 src/components/tess-unavailable.astro diff --git a/src/components/tess-listings.tsx b/src/components/tess-listings.tsx new file mode 100644 index 0000000..32ffdd6 --- /dev/null +++ b/src/components/tess-listings.tsx @@ -0,0 +1,136 @@ +import { useEffect, useState, type ComponentType, type ReactNode, type SVGProps } from 'react'; +import { BookOpenIcon, CalendarIcon, ExclamationTriangleIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline'; +import { + browseLinks, + eventDates, + eventLink, + eventPlace, + materialLink, + materials, + pastEvents, + upcomingEvents, + type TessResult, +} from '../lib/tess'; + +const ITEM = 'py-4 first:pt-0 last:pb-0'; +const ITEM_LINK = 'group -m-2 block rounded-lg p-2 transition-colors hover:bg-gray-50 dark:hover:bg-white/[0.04] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent'; +const ITEM_TITLE = 'font-semibold text-brand-primary dark:text-white transition-colors group-hover:text-accent'; +const LIST = 'divide-y divide-gray-200/60 dark:divide-gray-700/30'; + +/** Null while TESS is still being asked. */ +function useTess(load: () => Promise>): TessResult | null { + const [result, setResult] = useState | null>(null); + useEffect(() => { + load().then(setResult); + }, [load]); + return result; +} + +function Loading({ label }: { label: string }) { + return ( +
+ {label} +