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.
12 KiB
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.pyer 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, utenAPIRouter. 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.sqloginit.sqler innbyrdes inkonsistente (ulik representasjon av geodata) og gjenspeiler ikke faktisk skjema. Det reelle skjemaet skapes av 21ensure_*/ALTER TABLE IF NOT EXISTS-kall imain.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_jobog kjørt avworker.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-genaiiscrape_status.py, det eldregoogle-generativeaii de fire andre). scrape_nsg_3.pyogscrape_golfamore1.3.pyimporteres 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 avrooti 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 iapp/-roten i stedet for icomponents//en egen domain-mappe. - Ingen sentral, typet API-klient. Offentlige sider gjør rå
fetch()-kall spredt ipage.tsx-filer (23 forekomster i 15 filer). Admin har kunadminFetch()(10 linjer) som eneste fellesnevner, og den gjør bare 401→login-redirect, ingen typede responser eller feilhåndtering. src/middleware.tssjekker kun om cookienadmin_sessionfinnes, ikke om den er gyldig — reell verifisering skjer først i backend/route-handlers.- To identiske revalidate-endepunkter (
app/api/admin/revalidate-public/route.tsogapp/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,medlemskapunderadmin/) — strukturelt identisk state og logikk (drafts,toggleSelectAll,fetchDrafts, JSON-parse-fallback), bare med ulike feltnavn. Samme mønster er implementert en tredje gang inne iadmin/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.mjsslår eksplisitt av@typescript-eslint/no-explicit-anym.fl. — over 100 forekomster avanyi kildekoden, konsentrert i admin-sidene, til tross for attsconfig.jsonharstrict: 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.mder et reelt, godt skrevet designdokument med fargetokens iglobals.css, men etterlevelsen er svak: 1294 forekomster av vilkårligetext-[#...]/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.ymlkjører 4 tjenester:db(postgis/postgis:15-3.4),api,worker(samme image somapi, men unødvendig siden begge installerer Playwright+Chromium),frontend.- Backend-
Dockerfileer 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/Caddyfileruterteeoff.noogwww.teeoff.no, men inneholder også enteecup.teeoff.no-blokk som proxyer tilteecup-minio,teecup_apiogteecup_frontend— tjenester som ikke finnes i dette repoetsdocker-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)
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.- Tre "Washer"-admin-sider (
golfpakker,greenfee,medlemskap) + samme logikk igjen iadmin/page.tsx→ kandidat for en deltuseDraftReview-hook/generisk komponent. - Skjema-drift:
schema.sql/init.sql/migrations/gjenspeiler ikke faktisk databasestruktur, som i praksis styres avensure_*-funksjoner imain.py. Risiko for at en friskdocker compose upmedinit.sqlgir en annen database enn produksjon. - Duplisert scraper-boilerplate (Playwright-oppsett, Gemini-klient, feilmeldinger) — kunne vært samlet i
scrape_utils.py. - To identiske revalidate-endepunkter i frontend — trygt å fjerne det ene.
admin/page.tsxog de andre "god components" — bør deles opp i mindre komponenter etter hvert som de vedlikeholdes.- Ubrukte/utrangerte scripts (
scrape_nsg_3.py,scrape_golfamore1.3.py, to av tre admin-scripts,sync_greenfee.py/sync_weather_forecast.pyuten synlig cron) — bør enten dokumenteres som "kjør manuelt ved behov" eller fjernes. - To parallelle Gemini-SDK-er (
google-genaioggoogle-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.pyer 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: truei TypeScript undergraves av at ESLint sinno-explicit-anyer slått av — over 100any-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.pyer 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:
- Etabler én autoritativ kilde for databaseskjemaet (enten gå all-in på migrations-mappen, eller generer
schema.sqlfra faktisk produksjonsskjema) — reduserer risiko for miljøforskjeller. - Del opp
main.pyi routere per domene — gjør videre arbeid tryggere og raskere. - Slå sammen de tre "Washer"-admin-sidene til én gjenbrukbar komponent/hook — reduserer trippel vedlikeholdsbyrde ved neste endring i godkjenningsflyten.