Nye-TeeOff/docs/kodeanalyse-2026-07-25.md
Erol Haagenrud d0e60fc271 ikset og deployet. Kort oppsummering av endringen:
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.
2026-07-26 09:10:36 +02:00

106 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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, aprilmai 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 | Lavmiddels | Sekret-gjenbruk på tvers av sikkerhetsdomener — kompromittering ett sted lekker til et annet |
| `python-jose` (kjente CVE-er, algoritmeforvirring) og `passlib` (vedlikeholdsmodus siden ~2020) | Lavmiddels | 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.