From d0e60fc271e4076c3491d9c61d1fd77c6c5fddb2 Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Sun, 26 Jul 2026 09:10:36 +0200 Subject: [PATCH] ikset og deployet. Kort oppsummering av endringen: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit backend/weather_forecast.py: weather_sync_loop tar nå en valgfri on_updated-callback som kalles etter hver synkroniseringsrunde, men kun når noe faktisk ble oppdatert. backend/main.py: sender inn invalidate_public_api_caches(include_place_pages=True) som denne callbacken når værsynken startes ved oppstart. api-containeren er restartet og kjører rent. Effekten: neste gang værsynken faktisk endrer noe (skjer omtrent hver time), tømmes backendens interne cache og frontendens unstable_cache for anleggslisten/steds-sidene automatisk — så "Ikke regn meldt"-filteret vil vise fersk værdata i stedet for en fastfrosset snapshot. --- backend/main.py | 6 +- backend/weather_forecast.py | 14 ++++- docs/kodeanalyse-2026-07-25.md | 106 +++++++++++++++++++++++++++++++++ docs/oppgaver.md | 74 +++++++++++++++++++++++ 4 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 docs/kodeanalyse-2026-07-25.md create mode 100644 docs/oppgaver.md diff --git a/backend/main.py b/backend/main.py index 24fb2e6..68b172f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -3812,7 +3812,11 @@ async def lifespan(app: FastAPI): await ensure_public_query_indexes(conn) app.state.weather_sync_stop_event = asyncio.Event() app.state.weather_sync_task = asyncio.create_task( - weather_sync_loop(app.state.pool, app.state.weather_sync_stop_event) + weather_sync_loop( + app.state.pool, + app.state.weather_sync_stop_event, + on_updated=lambda: invalidate_public_api_caches(include_place_pages=True), + ) ) app.state.contact_submission_tracker = {} initialize_public_api_caches() diff --git a/backend/weather_forecast.py b/backend/weather_forecast.py index 1960344..456f317 100644 --- a/backend/weather_forecast.py +++ b/backend/weather_forecast.py @@ -428,7 +428,12 @@ async def sync_all_weather_forecasts(pool, *, force: bool = False) -> dict[str, return stats -async def weather_sync_loop(pool, stop_event: asyncio.Event) -> None: +async def weather_sync_loop( + pool, + stop_event: asyncio.Event, + *, + on_updated=None, +) -> None: await asyncio.sleep(WEATHER_SYNC_INITIAL_DELAY_SECONDS + random.uniform(0, 30)) while not stop_event.is_set(): @@ -441,6 +446,13 @@ async def weather_sync_loop(pool, stop_event: asyncio.Event) -> None: f"{stats['cached']} cachet, " f"{stats['failed']} feilet" ) + if stats["updated"] > 0 and on_updated is not None: + try: + result = on_updated() + if asyncio.iscoroutine(result): + await result + except Exception as exc: + print(f"Vær-sync: cache-invalidering feilet: {exc}") except Exception as exc: print(f"Vær-sync feilet på batch-nivå: {exc}") diff --git a/docs/kodeanalyse-2026-07-25.md b/docs/kodeanalyse-2026-07-25.md new file mode 100644 index 0000000..484a460 --- /dev/null +++ b/docs/kodeanalyse-2026-07-25.md @@ -0,0 +1,106 @@ +# Kodeanalyse — teeoff.no + +**Dato:** 2026-07-25 +**Omfang:** Hele repoet (`backend/` – Python/FastAPI, `frontend/` – Next.js 16/React 19, `migrations/`, `schema.sql`/`init.sql`, `deploy/Caddyfile`, `docker-compose*.yml`). +**Metode:** Statisk gjennomgang av kildekoden (ingen kjøring av app eller tester, siden det ikke finnes noen testsuite å kjøre). + +--- + +## 1. Oppsummering + +Teeoff.no er en golf-nettside med et offentlig nettsted (baneinfo, medlemskap, greenfee, VTG/simulatorer, redaksjonelt innhold) og et admin-panel for å kuratere data som hentes inn via automatiserte scraping-jobber (Playwright + Gemini LLM). Stack: FastAPI + asyncpg + PostgreSQL/PostGIS i backend, Next.js App Router + Tailwind i frontend, Caddy som reverse proxy i produksjon. + +Prosjektet fungerer og har et par genuint gjennomtenkte deler (SEO-laget, magic-link-autentisering, worker-kø for scraping). Hovedbildet er likevel et **enmanns-/AI-assistert prosjekt som har vokst raskt uten refaktorering underveis**: én 6500-linjers backend-fil, én 2634-linjers admin-side, ingen tester, ingen strukturert logging, og betydelig duplisert kode mellom de fem "godkjenn scrape-forslag"-admin-sidene. Ingen kritiske sikkerhetshull ble funnet (SQL-injeksjon ser ut til å være unngått konsekvent), men det er noen sikkerhetsrelaterte forbedringspunkter (rate limiting, secret-gjenbruk, avhengigheter uten versjonspinning). + +--- + +## 2. Arkitektur + +### Backend (`backend/`, ~12 200 linjer Python) + +- **`main.py` er 6501 linjer** og inneholder *alt*: konfig, cache, e-post/SMTP, IndexNow-integrasjon, Google OAuth, skjemamigrering (`ensure_*`-funksjoner), Pydantic-modeller og alle 72 REST-endepunktene — i én modul, uten `APIRouter`. Det finnes ingen inndeling i auth/admin/public/media-routere. +- Øverst i filen står en kommentar rettet mot en AI-kodeassistent: *"LOV: Aldri trunker eller slett logikk for 'effektivitet'"* — dette er sannsynligvis en medvirkende årsak til at filen har fått vokse ukontrollert i stedet for å bli delt opp. +- Ingen ORM — alt er rå, parameteriserte `asyncpg`-spørringer. Der SQL bygges dynamisk med f-strings, er det kun kolonnenavn (fra en eksplisitt allow-list) som settes sammen, aldri brukerdata direkte. +- **Databaseskjemaet finnes ikke ett sted.** `schema.sql` og `init.sql` er innbyrdes inkonsistente (ulik representasjon av geodata) og gjenspeiler ikke faktisk skjema. Det reelle skjemaet skapes av 21 `ensure_*`/`ALTER TABLE IF NOT EXISTS`-kall i `main.py`, som kjøres ved oppstart. `migrations/`-mappen brukes bare ad hoc (6 filer, april–mai 2026). Dette er reell skjema-drift. +- **Scraping**: fem aktive jobbtyper (banestatus, medlemskap, greenfee, VTG, golfpakker), hver i sin egen fil, lagt i kø via `enqueue_scrape_job` og kjørt av `worker.py` (solid kø-implementasjon med heartbeat, retry og feilklassifisering). Sterk duplisering av Playwright-oppsett og Gemini-kallmønster mellom scraperne. To ulike Gemini-SDK-er brukes samtidig (`google-genai` i `scrape_status.py`, det eldre `google-generativeai` i de fire andre). +- `scrape_nsg_3.py` og `scrape_golfamore1.3.py` importeres ingen steder — trolig utrangerte/manuelt kjørte scripts. +- Tre overlappende scripts for å administrere admin-brukere (`create_admin.py`, `bootstrap_admin_access.py`, `update_admin.py`) uten at de eldre er fjernet. +- `facility_contacts_export.csv` (87 KB) er committet til git og eid av `root` i filsystemet — tyder på at eksport-scriptet er kjørt automatisk/manuelt utenfor normal arbeidsflyt og sjekket inn ved en feil. + +### Frontend (`frontend/src/`, 85 filer) + +- App Router-strukturen er stort sett konsistent (`page.tsx` + `[slug]/page.tsx` + klientkomponent), men noen delte moduler (`FacilitySearch.tsx`, `facilityData.ts`, `seo.ts`) ligger løst i `app/`-roten i stedet for i `components/`/en egen domain-mappe. +- **Ingen sentral, typet API-klient.** Offentlige sider gjør rå `fetch()`-kall spredt i `page.tsx`-filer (23 forekomster i 15 filer). Admin har kun `adminFetch()` (10 linjer) som eneste fellesnevner, og den gjør bare 401→login-redirect, ingen typede responser eller feilhåndtering. +- `src/middleware.ts` sjekker kun *om* cookien `admin_session` finnes, ikke om den er gyldig — reell verifisering skjer først i backend/route-handlers. +- To identiske revalidate-endepunkter (`app/api/admin/revalidate-public/route.ts` og `app/internal/revalidate-public/route.ts`, 92 linjer, byte-for-byte like) — rester fra en migrering som ikke ble ryddet opp. +- **Betydelig duplisering** mellom de tre "gjennomgå scrape-forslag"-admin-sidene (`golfpakker`, `greenfee`, `medlemskap` under `admin/`) — strukturelt identisk state og logikk (`drafts`, `toggleSelectAll`, `fetchDrafts`, JSON-parse-fallback), bare med ulike feltnavn. Samme mønster er implementert en *tredje* gang inne i `admin/page.tsx`. +- **Store "god components"**: `admin/page.tsx` (2634 linjer), `EditFacilityClient.tsx` (1615), `FacilityDetailView.tsx` (1468), `FacilitySearch.tsx` (1300), `admin/artikler/page.tsx` (1144), `SimulatorAdminClient.tsx` (1024), `admin/sider/page.tsx` (960). +- `eslint.config.mjs` slår eksplisitt av `@typescript-eslint/no-explicit-any` m.fl. — over 100 forekomster av `any` i kildekoden, konsentrert i admin-sidene, til tross for at `tsconfig.json` har `strict: true`. +- **Ingen tester**: ingen jest/vitest/playwright-oppsett, ingen test-script i `package.json`. +- SEO-laget (`seo.ts`, `pageSeo.ts`, `sitemap.ts`, `robots.ts`) er det mest gjennomarbeidede i frontend — konsistent, godt strukturert, i tydelig kontrast til resten. +- `docs/design-system.md` er et reelt, godt skrevet designdokument med fargetokens i `globals.css`, men etterlevelsen er svak: 1294 forekomster av vilkårlige `text-[#...]`/`bg-[#...]`-Tailwind-klasser, mange med hex-verdier som ikke finnes i det dokumenterte palettet — et de facto parallelt fargesett har vokst frem, i strid med dokumentets egen anbefaling. + +### Deploy/infra + +- `docker-compose.yml` kjører 4 tjenester: `db` (postgis/postgis:15-3.4), `api`, `worker` (samme image som `api`, men unødvendig siden begge installerer Playwright+Chromium), `frontend`. +- Backend-`Dockerfile` er single-stage på `python:3.11-slim`, installerer full Playwright+Chromium-stack i samme image som brukes til API-serveren (som ikke trenger nettleseren) — unødvendig stort image, ingen non-root-bruker. +- `deploy/Caddyfile` ruter `teeoff.no` og `www.teeoff.no`, men inneholder også en `teecup.teeoff.no`-blokk som proxyer til `teecup-minio`, `teecup_api` og `teecup_frontend` — tjenester som **ikke finnes i dette repoets `docker-compose.yml`**, og en kommentar som viser til "CLAUDE.md-status" i et annet prosjekt. Dette virker å være en separat, relatert applikasjon ("teecup") som deler samme server/Caddy-instans, men konfigurasjonen for den ligger ikke i dette repoet — verdt å avklare om det er tilsiktet. + +--- + +## 3. Sikkerhet + +| Funn | Alvorlighet | Kommentar | +|---|---|---| +| Ingen rate limiting/brute-force-beskyttelse på `/api/auth/login` | Middels | 2FA (TOTP) reduserer risikoen, men passordfeltet kan brute-forces ubegrenset | +| `PUBLIC_SESSION_SECRET` faller tilbake til `JWT_SECRET`; `FRONTEND_REVALIDATE_SECRET` faller tilbake til samme igjen | Lav–middels | Sekret-gjenbruk på tvers av sikkerhetsdomener — kompromittering ett sted lekker til et annet | +| `python-jose` (kjente CVE-er, algoritmeforvirring) og `passlib` (vedlikeholdsmodus siden ~2020) | Lav–middels | Vurder `pyjwt` + aktivt vedlikeholdt hash-bibliotek | +| `requirements.txt` uten versjonspinning i det hele tatt | Middels | Ingen reproduserbare builds; sikkerhetsoppdateringer/brytende endringer innføres usporet | +| Login-endepunkter tar `data: dict` i stedet for Pydantic-modeller | Lav | Ingen automatisk input-validering/typesjekk på disse rutene | +| Dockerfile: ingen non-root-bruker, `COPY . .` inkluderer data-/CSV-filer | Lav | Standard hardening-forbedringer | +| SQL-injeksjon | Ingen funn | Konsekvent bruk av parameteriserte queries; dynamisk SQL bygges kun fra allow-listede kolonnenavn | +| Magic-link-innlogging | Ingen funn | Token hashes før lagring, cooldown og enumereringsbeskyttelse er korrekt implementert | +| Path traversal i `uploads/[...path]/route.ts` | Ingen funn | Eksplisitt `startsWith(uploadsRoot)`-sjekk | + +--- + +## 4. Teknisk gjeld / duplisering (prioritert etter antatt gevinst) + +1. **`main.py` (6501 linjer)** bør deles i moduler/`APIRouter`-er (auth, admin, public, media, scraping-triggere). Størst risiko for feil ved videre utvikling, vanskeligst å navigere. +2. **Tre "Washer"-admin-sider** (`golfpakker`, `greenfee`, `medlemskap`) + samme logikk *igjen* i `admin/page.tsx` → kandidat for en delt `useDraftReview`-hook/generisk komponent. +3. **Skjema-drift**: `schema.sql`/`init.sql`/`migrations/` gjenspeiler ikke faktisk databasestruktur, som i praksis styres av `ensure_*`-funksjoner i `main.py`. Risiko for at en frisk `docker compose up` med `init.sql` gir en annen database enn produksjon. +4. **Duplisert scraper-boilerplate** (Playwright-oppsett, Gemini-klient, feilmeldinger) — kunne vært samlet i `scrape_utils.py`. +5. **To identiske revalidate-endepunkter** i frontend — trygt å fjerne det ene. +6. **`admin/page.tsx` og de andre "god components"** — bør deles opp i mindre komponenter etter hvert som de vedlikeholdes. +7. **Ubrukte/utrangerte scripts** (`scrape_nsg_3.py`, `scrape_golfamore1.3.py`, to av tre admin-scripts, `sync_greenfee.py`/`sync_weather_forecast.py` uten synlig cron) — bør enten dokumenteres som "kjør manuelt ved behov" eller fjernes. +8. **To parallelle Gemini-SDK-er** (`google-genai` og `google-generativeai`) installert samtidig — bør konsolideres til én. + +--- + +## 5. Kodekvalitet generelt + +- **Ingen testdekning** i noen del av kodebasen (verken backend eller frontend). `test_login.py`/`test_gemini.py` er manuelle debug-scripts, ikke en pytest-suite. +- **Ingen strukturert logging** i backend — kun spredte `print()`-kall med emoji-prefiks; ingen loggnivåer, avhengig av at Docker fanger stdout. +- Flere stumme `except:`-klausuler som svelger alle feil (`sync_greenfee.py`, `scrape_nsg_3.py`, `import_wp.py`). +- Frontend: `strict: true` i TypeScript undergraves av at ESLint sin `no-explicit-any` er slått av — over 100 `any`-treff. +- Kommentarer i stil "REGEL 1: ALDRI trunker eller fjern data", "RETTING: Flyttet NextRequest for å fikse build-error" tyder på at store deler av koden er skrevet iterativt med en AI-assistent uten etterfølgende opprydning — nyttig å vite når man vurderer hvor mye av koden som er bevisst arkitektur vs. ad hoc-lapping. + +--- + +## 6. Det som fungerer bra + +- **Magic-link- og OAuth-autentisering** er korrekt implementert med enumereringsbeskyttelse og token-hashing. +- **`worker.py`** er en solid, godt strukturert jobbkø med heartbeat, backoff og feilklassifisering. +- **SEO-laget** i frontend (JSON-LD, sitemap, robots, OG-bilder) er gjennomtenkt og konsistent. +- **SQL-sikkerhet**: bevisst og konsekvent bruk av parameteriserte queries selv uten ORM. +- **Design-system-dokumentet** (`docs/design-system.md`) er et godt utgangspunkt — problemet er etterlevelse, ikke dokumentet selv. + +--- + +## 7. Anbefalte neste steg (prioritert) + +Se `docs/oppgaver.md` for en konkret, sporbar oppgaveliste basert på funnene over. De tre høyest prioriterte tiltakene er: + +1. Etabler én autoritativ kilde for databaseskjemaet (enten gå all-in på migrations-mappen, eller generer `schema.sql` fra faktisk produksjonsskjema) — reduserer risiko for miljøforskjeller. +2. Del opp `main.py` i routere per domene — gjør videre arbeid tryggere og raskere. +3. Slå sammen de tre "Washer"-admin-sidene til én gjenbrukbar komponent/hook — reduserer trippel vedlikeholdsbyrde ved neste endring i godkjenningsflyten. diff --git a/docs/oppgaver.md b/docs/oppgaver.md new file mode 100644 index 0000000..a193810 --- /dev/null +++ b/docs/oppgaver.md @@ -0,0 +1,74 @@ +# Oppgaver — teeoff.no + +Løpende referanse for arbeid på prosjektet. Bakgrunn og detaljerte funn står i [`kodeanalyse-2026-07-25.md`](./kodeanalyse-2026-07-25.md). + +**Slik brukes filen:** +- Nye oppgaver legges under riktig seksjon med dato i parentes. +- Når noe er avklart og klart til å jobbes med, flytt det fra "Trenger avklaring" til "Klar til å gjøres". +- Når noe er fullført, flytt det til "Gjort" nederst med dato og ev. commit-referanse. + +--- + +## 🔴 Sikkerhet (klar til å gjøres) + +- [ ] Legg til rate limiting/lockout på `/api/auth/login` (backend/main.py) — ingen brute-force-beskyttelse på passordfeltet i dag utover 2FA. (2026-07-25) +- [ ] Fjern secret-fallback-kjeden: `PUBLIC_SESSION_SECRET` → `JWT_SECRET` → `FRONTEND_REVALIDATE_SECRET`. Gi hver sitt eget, obligatoriske secret. (2026-07-25) +- [ ] Lås versjoner i `backend/requirements.txt` (i dag helt uten pinning). (2026-07-25) +- [ ] Erstatt `python-jose` (kjente CVE-er) og vurder om `passlib` (vedlikeholdsmodus) bør byttes ut. (2026-07-25) +- [ ] Fjern `facility_contacts_export.csv` fra git (committet ved en feil, eid av root i filsystemet) og legg til i `.gitignore`. (2026-07-25) + +## 🧹 Teknisk gjeld / refaktorering (klar til å gjøres) + +- [ ] Del `backend/main.py` (6501 linjer) opp i `APIRouter`-moduler: auth, admin, public/facilities, media, scraping-triggere. (2026-07-25) +- [ ] Slå sammen `admin/golfpakker`, `admin/greenfee`, `admin/medlemskap` (frontend) til én gjenbrukbar `useDraftReview`-hook/komponent — i dag nesten identisk kode tre (fire, inkl. `admin/page.tsx`) steder. (2026-07-25) +- [ ] Fjern ett av de to identiske revalidate-endepunktene: `frontend/src/app/api/admin/revalidate-public/route.ts` vs. `frontend/src/app/internal/revalidate-public/route.ts`. (2026-07-25) +- [ ] Samle duplisert Playwright-/Gemini-boilerplate fra scraperne (`scrape_golfpakker.py`, `scrape_greenfee.py`, `scrape_membership.py`, `scrape_vtg.py`, `scrape_status.py`) i `scrape_utils.py`. (2026-07-25) +- [ ] Konsolider til én Gemini-SDK — i dag installeres både `google-genai` og `google-generativeai`, brukt inkonsekvent på tvers av scraperne. (2026-07-25) +- [ ] Splitt opp de største "god components" i frontend etter hvert som de vedlikeholdes: `admin/page.tsx` (2634 linjer), `EditFacilityClient.tsx` (1615), `FacilityDetailView.tsx` (1468), `FacilitySearch.tsx` (1300), `admin/artikler/page.tsx` (1144), `SimulatorAdminClient.tsx` (1024). (2026-07-25) +- [ ] Flytt delte moduler ut av `app/`-roten til `components/`/en domain-mappe: `FacilitySearch.tsx`, `facilityData.ts`, `seo.ts`. (2026-07-25) +- [ ] Bygg egen, mindre Docker-image for `api`-tjenesten uten Playwright/Chromium (kun `worker` trenger nettleseren). (2026-07-25) + +## ✅ Kvalitet / prosess (klar til å gjøres, lavere prioritet) + +- [ ] Innfør strukturert logging i backend i stedet for spredte `print()`-kall. (2026-07-25) +- [ ] Erstatt stumme `except:`-klausuler (`sync_greenfee.py`, `scrape_nsg_3.py`, `import_wp.py`) med eksplisitt feilhåndtering/logging. (2026-07-25) +- [ ] Skriv et minimum av automatiserte tester (backend: pytest for auth-flyt og kritiske endepunkter; frontend: i det minste ett rammeverk satt opp) — i dag null testdekning. (2026-07-25) +- [ ] Rydd opp i vilkårlige `text-[#...]`/`bg-[#...]`-hex-klasser (1294 forekomster) til fordel for tokens fra `docs/design-system.md`/`globals.css`. (2026-07-25) +- [ ] Vurder å slå på `@typescript-eslint/no-explicit-any` igjen og rydde de 100+ eksisterende `any`-bruken gradvis. (2026-07-25) + +--- + +## 🌍 Internasjonal ekspansjon (planlagt, faset) (2026-07-25) + +Bakgrunn og vurderte alternativer i [`kodeanalyse-2026-07-25.md`](./kodeanalyse-2026-07-25.md)-samtalen. Retningsvalg tatt 2026-07-25: full flerspråklig side (ikke bare DB-oversettelse av innhold), og normalisert `hole_points`-modell for hullkoordinater. + +**Fase 1 — Databaselag** +- [ ] Migrasjon: legg til `country_code` (ISO 3166-1 alpha-2) på anleggstabellen, backfill `'NO'` på alle eksisterende rader, gjør feltet obligatorisk uten default fremover. +- [ ] Avklar region/fylke-modell for ikke-norske anlegg (dagens `REGIONS`/fylkesfilter er Norge-spesifikt). +- [ ] Migrasjon: `*_translations`-tabeller for redaksjonelt innhold (f.eks. `facility_translations(facility_id, locale, description, ...)`, tilsvarende for artikler), én rad per locale, med fallback til `'no'`. +- [ ] Migrasjon: `course_holes(id, facility_id, hole_number, par, ...)` + `hole_points(id, hole_id, point_type, geom geography(Point,4326))` med `point_type` som enum (`green_front`, `green_center`, `green_back`, utvidbart til teested/hasard senere). +- [ ] Bygg admin-verktøy for å registrere hull-punkter (trolig kart-klikk via Leaflet, siden `FacilityDetailLeafletMap.tsx` allerede finnes). + +**Fase 2 — UI-i18n-infrastruktur** +- [ ] Velg og sett opp i18n-rammeverk for frontend (f.eks. `next-intl`) med locale-prefikset routing. +- [ ] **Avklar**: skal URL-slugs oversettes per språk (`/en/golf-courses`) eller beholdes norske på alle språk (`/en/golfbaner`)? Må avgjøres før routing-arbeidet starter. +- [ ] Oppdater SEO-laget (`seo.ts`, `sitemap.ts`, `robots.ts`) med hreflang og per-locale sitemap/robots. +- [ ] Bygg språkvelger i UI. + +**Fase 3 — Oversettelse av faktisk innhold/UI** +- [ ] Trekk ut hardkodet norsk UI-tekst til meldingskataloger. +- [ ] Oversett/legg inn innhold for de første ikke-norske anleggene. + +## ❓ Trenger avklaring + +- [ ] **`teecup`-blokken i `deploy/Caddyfile`** ruter til `teecup-minio`, `teecup_api`, `teecup_frontend` — tjenester som ikke finnes i dette repoets `docker-compose.yml`, og viser til "CLAUDE.md-status" i tilsynelatende et annet prosjekt. Er "teecup" en separat applikasjon som deler server med teeoff? Bør det dokumenteres her, eller hører Caddyfile-blokken ikke hjemme i dette repoet? (2026-07-25) +- [ ] **Skjema-kilde**: Skal `schema.sql`/`init.sql` oppdateres til å reflektere faktisk skjema (slik det skapes av `ensure_*`-funksjonene i `main.py`), eller skal man gå motsatt vei og fjerne `ensure_*`-migreringen til fordel for filene i `migrations/`? Trenger en beslutning om hvilken tilnærming som er "sannheten" videre. (2026-07-25) +- [ ] **Ubrukte scripts**: `scrape_nsg_3.py`, `scrape_golfamore1.3.py`, `sync_greenfee.py`, `sync_weather_forecast.py` importeres ikke av verken `main.py` eller `worker.py`. Kjøres disse fortsatt manuelt/via ekstern cron på serveren? Skal de beholdes, dokumenteres som manuelle verktøy, eller slettes? (2026-07-25) +- [ ] **Admin-brukerscripts**: `create_admin.py`, `bootstrap_admin_access.py`, `update_admin.py` overlapper — sistnevntes docstring hevder å være en "trygg erstatning" for de to andre. Kan de to eldre slettes, eller er det en grunn til at alle tre fortsatt finnes? (2026-07-25) +- [ ] **Dashboard vs. dedikerte admin-sider**: `admin/page.tsx` inneholder faner som gjør samme jobb som de dedikerte `admin/golfpakker`, `admin/greenfee`, `admin/medlemskap`, `admin/vtg`-sidene. Er begge i aktiv bruk, eller kan én av de to variantene fases ut? Påvirker hvilken retning refaktoreringen i "Washer"-oppgaven over bør ta. (2026-07-25) + +--- + +## Gjort + +- [x] **"Ikke regn meldt"-filteret viste stale værdata** — `weather_sync_loop` (backend/weather_forecast.py) oppdaterte `facility_weather_forecast` hver time, men kalte aldri `invalidate_public_api_caches()`, så frontendens `unstable_cache` (uten tidsbasert utløp) og backendens interne cache ble aldri fornyet av værsynken — kun av urelaterte admin-redigeringer. Løst ved å legge til en `on_updated`-callback i `weather_sync_loop` som trigger `invalidate_public_api_caches(include_place_pages=True)` når noe faktisk er oppdatert. `api`-containeren restartet. (2026-07-26)