# 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.