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.
106 lines
12 KiB
Markdown
106 lines
12 KiB
Markdown
# 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.
|