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

12 KiB
Raw Permalink Blame History

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.