From 1ca1868e1163550154b6ae7b2ceb897d69d3c62b Mon Sep 17 00:00:00 2001 From: Erol Date: Wed, 4 Mar 2026 13:17:10 +0100 Subject: [PATCH] =?UTF-8?q?F=C3=B8r=20LLM=20og=20AII=20skal=20synkronisere?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/.env | 3 +- backend/__pycache__/main.cpython-311.pyc | Bin 14340 -> 14340 bytes backend/requirements.txt | 3 +- backend/scrape_status.py | 28 + backend/test_login.py | 47 ++ backend/update_admin.py | 85 +++ fil-tre.txt | 12 + frontend/src/app/admin/login/page.tsx | 4 +- kode_eksport_1/backend_create_admin_py.txt | 56 +- kode_eksport_1/backend_main_py.txt | 15 +- kode_eksport_1/backend_scrape_status_py.txt | 104 ++- kode_eksport_1/backend_update_admin_py.txt | 52 ++ .../frontend_src_app_admin_login_page_tsx.txt | 1 + kode_eksport_1/test_tjome_py.txt | 44 ++ struktur3_dump.txt | 622 ++++++++++++++++++ test_tjome.py | 44 ++ 16 files changed, 1062 insertions(+), 58 deletions(-) create mode 100644 backend/test_login.py create mode 100644 backend/update_admin.py create mode 100644 kode_eksport_1/backend_update_admin_py.txt create mode 100644 kode_eksport_1/test_tjome_py.txt create mode 100644 struktur3_dump.txt create mode 100644 test_tjome.py diff --git a/backend/.env b/backend/.env index de45315..5cd6024 100644 --- a/backend/.env +++ b/backend/.env @@ -2,4 +2,5 @@ SMTP_SERVER=send.one.com SMTP_PORT=465 SMTP_USER=teeoff@teeoff.no SMTP_PASS=Shallot Distress43, Serving Smog Hangnail Shower -EMAIL_TO=erol.haagenrud@teeoff.no \ No newline at end of file +EMAIL_TO=erol.haagenrud@teeoff.no +GEMINI_API_KEY=AIzaSyDX_WCvZcH3Z8xRpH-XWaoeVYWuE0Wrlog \ No newline at end of file diff --git a/backend/__pycache__/main.cpython-311.pyc b/backend/__pycache__/main.cpython-311.pyc index a958cb97533dceabb18bffd6231d8f728b24dfb8..0226cfe7a38f1dfdde8df6e6ec7fb593c1ae7ae5 100644 GIT binary patch delta 20 acmZoEXer=c&dbZi00a+ZR&3<{X$}BB;Ra9u delta 20 acmZoEXer=c&dbZi00bQMOE+@=GzS1XH3ioI diff --git a/backend/requirements.txt b/backend/requirements.txt index ff5702f..97d1232 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -10,4 +10,5 @@ apscheduler python-dotenv python-jose[cryptography] passlib[bcrypt] -pyotp \ No newline at end of file +pyotp +google-generativeai \ No newline at end of file diff --git a/backend/scrape_status.py b/backend/scrape_status.py index baf53fd..0a18990 100644 --- a/backend/scrape_status.py +++ b/backend/scrape_status.py @@ -106,6 +106,34 @@ async def run_daily_scraping(): warnings.append(f"❌ {f['name']}: Fant ikke elementet '{f['scrape_status_selector']}' i iframen") continue full_text = await element.inner_text() + + elif method == 'click_then_css': + # Vi forventer formatet: "knappe_selector||tekst_selector" + parts = f['scrape_status_selector'].split('||') + if len(parts) != 2: + warnings.append(f"❌ {f['name']}: Ugyldig selector for click_then_css (mangler ||)") + continue + + btn_selector, text_selector = parts + + # 1. Finn og klikk på knappen + btn = page.locator(btn_selector).first + if await btn.count() == 0: + warnings.append(f"❌ {f['name']}: Fant ikke knappen å klikke på: '{btn_selector}'") + continue + + await btn.click() + + # 2. Vent 2 sekunder så animasjonen (sidepanelet) rekker å bli ferdig + await asyncio.sleep(2) + + # 3. Les av teksten + element = page.locator(text_selector).first + if await element.count() == 0: + warnings.append(f"❌ {f['name']}: Fant ikke tekstboksen '{text_selector}' etter klikk") + continue + + full_text = await element.inner_text() else: warnings.append(f"⚠️ {f['name']}: Ukjent skrapemetode i databasen: '{method}'") diff --git a/backend/test_login.py b/backend/test_login.py new file mode 100644 index 0000000..f026cbf --- /dev/null +++ b/backend/test_login.py @@ -0,0 +1,47 @@ +import asyncio +import asyncpg +import os +from passlib.context import CryptContext + +DB_URL = os.getenv("DATABASE_URL", "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff") + +# Vi setter opp passord-sjekkeren AKKURAT slik main.py gjør det +pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto") + +async def test_sannheten(): + print("\n" + "="*50) + print(" 🔍 TEE OFF SANNHETSSERUM") + print("="*50) + + username = "Envide Webutvikling" + test_password = "Solveig Vilde Ingvild Gina" # Sørg for at dette er det du satte sist! + + try: + conn = await asyncpg.connect(DB_URL) + row = await conn.fetchrow("SELECT password_hash FROM admins WHERE username = $1", username) + + if not row: + print("❌ FEIL: Fant ikke brukeren i det hele tatt!") + return + + db_hash = row['password_hash'] + print(f"1. Hash funnet i databasen: {db_hash[:30]}...") + + print(f"2. Tester mot passordet: '{test_password}'") + + # Den magiske testen + is_valid = pwd_context.verify(test_password, db_hash) + + print("-" * 50) + if is_valid: + print("✅ SUKSESS! Passordet og hashen stemmer 100% overens.") + print("➡️ KONKLUSJON: Hashingen fungerer perfekt. Problemet MÅ være at FastAPI (main.py) ikke klarer å lese JSON-dataene fra curl/frontend riktig.") + else: + print("❌ FEIL! Passordet stemmer IKKE med hashen i databasen.") + print("➡️ KONKLUSJON: Scriptet som oppdaterer passordet gjør en feil (f.eks. legger til usynlige tegn), eller lagringen i databasen blir korrupt.") + + finally: + await conn.close() + +if __name__ == "__main__": + asyncio.run(test_sannheten()) \ No newline at end of file diff --git a/backend/update_admin.py b/backend/update_admin.py new file mode 100644 index 0000000..4883f01 --- /dev/null +++ b/backend/update_admin.py @@ -0,0 +1,85 @@ +""" +TEE OFF ADMIN PASSWORD UPDATER (API CONTAINER VERSION) +--------------------------------------------------------------------------- +FUNKSJON: Kobler direkte til databasen inni API-containeren, sjekker at + brukeren finnes, og utfører passordoppdateringen automatisk. +STATUS: Påvirker IKKE tofaktor (2FA). Gjør jobben fra start til slutt. +--------------------------------------------------------------------------- +""" +import asyncio +import asyncpg +import os +import sys +import getpass +from passlib.hash import pbkdf2_sha256 + +# Henter database-URL fra miljøvariabler (samme metode som backenden din bruker) +DB_URL = os.getenv("DATABASE_URL", "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff") + +async def update_admin_password(): + print("\n" + "="*50) + print(" TEE OFF ADMIN PASSORD-OPPDATERER (DIREKTE TILKOBLING)") + print("="*50) + + # Kobler til databasen på ekte backend-vis + try: + conn = await asyncpg.connect(DB_URL) + except Exception as e: + print(f"❌ Kunne ikke koble til databasen: {e}") + sys.exit(1) + + try: + # Brukernavn-verifisering + while True: + username = input("Brukernavn på admin som skal oppdateres: ").strip() + + print("⏳ Sjekker databasen...") + # Spør databasen direkte hvor mange som har dette navnet + count = await conn.fetchval("SELECT COUNT(*) FROM admins WHERE username = $1", username) + + if count == 0: + print(f"❌ Fant ingen bruker med navnet '{username}'. Prøv igjen.\n") + elif count > 1: + print(f"⚠️ KRITISK FEIL: Fant {count} brukere med navnet '{username}'. Avbryter.") + sys.exit(1) + else: + print(f"✅ Bruker '{username}' funnet i databasen!\n") + break + + # Passord-verifisering + while True: + password = getpass.getpass("Skriv inn NYTT passord: ") + password_confirm = getpass.getpass("Gjenta NYTT passord: ") + + if password == password_confirm: + if len(password) < 8: + print("⚠️ Advarsel: Passordet bør være minst 8 tegn.") + print(f"\n[DEBUG] Passord akseptert.") + break + else: + print("❌ Passordene er ikke like. Prøv igjen.\n") + + print("⏳ Genererer PBKDF2-hash...") + password_hash = pbkdf2_sha256.hash(password) + + print("⏳ Oppdaterer databasen automatisk...") + # Utfører selve oppdateringen (sikret mot SQL-injeksjoner) + await conn.execute("UPDATE admins SET password_hash = $1 WHERE username = $2", password_hash, username) + + print("\n✅ PASSORD OPPDATERT VELLYKKET!") + print("-" * 50) + print(f"Passordet for '{username}' er nå endret i databasen.") + print("Tofaktor (2FA) og alt annet er beholdt urørt.") + print("-" * 50 + "\n") + + finally: + # Lukk tilkoblingen pent + await conn.close() + +if __name__ == "__main__": + try: + # Siden vi bruker asyncpg, må scriptet kjøres i en asyncio-loop + asyncio.run(update_admin_password()) + except KeyboardInterrupt: + print("\nAvbrutt.") + sys.exit(0) \ No newline at end of file diff --git a/fil-tre.txt b/fil-tre.txt index 7a33a89..26d6c9c 100644 --- a/fil-tre.txt +++ b/fil-tre.txt @@ -1,7 +1,9 @@ 📁 teeoff/ + 📄 test_tjome.py 📄 fil-tre.txt 📄 struktur2_dump.txt 📄 seed.sql + 📄 struktur3_dump.txt 📄 eksport_script.py 📄 update_golfbox.sql 📄 docker-compose.yml @@ -682,22 +684,32 @@ 📄 page.tsx 📁 kode_eksport_1/ 📄 frontend_src_components_Header_tsx.txt + 📄 backend_scrape_nsg_3_py.txt 📄 frontend_next-env_d_ts.txt 📄 frontend_src_app_layout_tsx.txt 📄 frontend_src_app_page_tsx.txt 📄 eksport_script_py.txt 📄 frontend_src_app_golfbaner_[slug]_page_tsx.txt + 📄 backend_import_wp_py.txt 📄 frontend_src_middleware_ts.txt + 📄 test_tjome_py.txt 📄 frontend_src_app_golfbaner_[slug]_CourseDisplay_tsx.txt 📄 frontend_next_config_ts.txt 📄 frontend_src_app_admin_login_page_tsx.txt 📄 frontend_src_app_golfbaner_[slug]_FacilityDetailView_tsx.txt + 📄 backend_main_py.txt 📄 frontend_src_app_admin_page_tsx.txt 📄 frontend_src_app_HeroSlider_tsx.txt + 📄 backend_create_admin_py.txt + 📄 backend_sync_greenfee_py.txt + 📄 backend_scrape_status_py.txt + 📄 backend_scrape_golfamore1_3_py.txt 📄 frontend_src_app_FacilitySearch_tsx.txt 📄 frontend_src_config_constants_ts.txt + 📄 backend_import_gallery_py.txt 📁 backend/ 📄 scrape_nsg_3.py + 📄 update_admin.py 📄 import_gallery.py 📄 .env 📄 sync_greenfee.py diff --git a/frontend/src/app/admin/login/page.tsx b/frontend/src/app/admin/login/page.tsx index 9f19401..084efbd 100644 --- a/frontend/src/app/admin/login/page.tsx +++ b/frontend/src/app/admin/login/page.tsx @@ -83,8 +83,8 @@ export default function AdminLogin() {
{step === 1 ? ( <> - setFormData({...formData, username: e.target.value})} required /> - setFormData({...formData, password: e.target.value})} required /> + setFormData(prevState => ({...prevState, username: e.target.value}))} required /> + setFormData(prevState => ({...prevState, password: e.target.value}))} required /> ) : (
diff --git a/kode_eksport_1/backend_create_admin_py.txt b/kode_eksport_1/backend_create_admin_py.txt index f44898f..c2d3dcb 100644 --- a/kode_eksport_1/backend_create_admin_py.txt +++ b/kode_eksport_1/backend_create_admin_py.txt @@ -1,44 +1,64 @@ """ -TEE OFF ADMIN GENERATOR v1.3 (PBKDF2 ONLY) +TEE OFF ADMIN GENERATOR v1.9 (DEBUG & BULLETPROOF) --------------------------------------------------------------------------- -FUNKSJON: Genererer SQL-kommando for PBKDF2-basert administrator. +FUNKSJON: Genererer SQL-kommando for administrator. +STATUS: Beholder TRUNCATE for feilsøking, men sikrer SQL-innsendingen. --------------------------------------------------------------------------- """ import pyotp from passlib.hash import pbkdf2_sha256 import getpass +import sys def generate_admin(): print("\n" + "="*50) - print(" TEE OFF ADMIN GENERATOR v1.3") + print(" TEE OFF ADMIN GENERATOR v1.9 (DEBUG MODE)") print("="*50) username = input("Brukernavn (f.eks Envide Webutvikling): ").strip() email = input("E-post: ").strip() - password = getpass.getpass("Passord: ") + # Sikre mot SQL-feil hvis navnet/eposten inneholder apostrof + safe_username = username.replace("'", "''") + safe_email = email.replace("'", "''") + + # Passord-verifisering + while True: + password = getpass.getpass("Skriv inn passord: ") + password_confirm = getpass.getpass("Gjenta passord: ") + + if password == password_confirm: + if len(password) < 8: + print("⚠️ Advarsel: Passordet bør være minst 8 tegn.") + print(f"\n[DEBUG] Passord akseptert. Lengde: {len(password)} tegn.") + break + else: + print("❌ Passordene er ikke like. Prøv igjen.\n") + otp_secret = pyotp.random_base32() print("⏳ Genererer PBKDF2-hash...") - # Dette vil produsere en streng som starter med $pbkdf2-sha256$... password_hash = pbkdf2_sha256.hash(password) + print(f"[DEBUG] Hash generert. Lengde: {len(password_hash)} tegn.") print("\n✅ GENERERING VELLYKKET!") print("-" * 50) - print("1. KJØR DISSE TO KOMMANDOENE I REKKEFØLGE:") + print("SLIK LEGGER DU INN BRUKEREN TRYGT:") print("-" * 50) - - # Kommando 1: Tøm tabellen for å fjerne gamle, inkompatible hasher - print(f'docker exec -it teeoff_db psql -U teeoff_admin -d teeoff -c "TRUNCATE admins;"') - - # Kommando 2: Sett inn den nye brukeren - sql = f"INSERT INTO admins (username, email, password_hash, otp_secret) VALUES ('{username}', '{email}', '{password_hash}', '{otp_secret}');" - print(f'\ndocker exec -it teeoff_db psql -U teeoff_admin -d teeoff -c "{sql}"') - - print("\n" + "-" * 50) - print("2. KONFIGURER 2FA I GOOGLE AUTHENTICATOR:") - print(f"Nøkkel: {otp_secret}") + print("1. Gå inn i databasen:") + print(" docker exec -it teeoff_db psql -U teeoff_admin -d teeoff") + print("\n2. Lim inn disse to linjene nøyaktig slik de står:") + print("TRUNCATE admins;") + print(f"INSERT INTO admins (username, email, password_hash, otp_secret) VALUES ('{safe_username}', '{safe_email}', '{password_hash}', '{otp_secret}');") + print("\n3. Skriv 'exit' for å gå ut.") + print("-" * 50) + print("4. KONFIGURER 2FA I GOOGLE AUTHENTICATOR:") + print(f"Bruk denne nøkkelen: {otp_secret}") print("-" * 50 + "\n") if __name__ == "__main__": - generate_admin() \ No newline at end of file + try: + generate_admin() + except KeyboardInterrupt: + print("\nAvbrutt.") + sys.exit(0) \ No newline at end of file diff --git a/kode_eksport_1/backend_main_py.txt b/kode_eksport_1/backend_main_py.txt index 58d2a09..d57f6e1 100644 --- a/kode_eksport_1/backend_main_py.txt +++ b/kode_eksport_1/backend_main_py.txt @@ -126,7 +126,7 @@ app.add_middleware( @app.post("/api/auth/login") async def login(data: dict): """Steg 1: Sjekk passord og returner temp_token for 2FA.""" - print(f"🔐 Login-forsøk for: {data.get('username')}") + print(f"🔐 Loggin-forsøk for: {data.get('username')}") async with app.state.pool.acquire() as conn: admin = await conn.fetchrow( @@ -138,17 +138,20 @@ async def login(data: dict): print(" - ❌ Bruker ikke funnet i databasen") raise HTTPException(status_code=401, detail="Ugyldig brukernavn eller passord") - # DEBUG: Printer starten på hashen for å verifisere formatet ($pbkdf2-sha256$...) h = admin['password_hash'] print(f" - Verifiserer hash i DB (starter med: {h[:20]}...)") + # FIKS: Vi pakker KUN selve verify-sjekken inn i try/except try: - if not pwd_context.verify(data.get('password'), h): - print(" - ❌ Passordet samsvarer ikke med hashen") - raise HTTPException(status_code=401, detail="Ugyldig brukernavn eller passord") + is_valid = pwd_context.verify(data.get('password'), h) except Exception as e: - print(f" - 🔥 FEIL VED VERIFISERING: {e}") + print(f" - 🔥 FEIL VED LESING AV HASH: {e}") raise HTTPException(status_code=500, detail="Internt problem med passord-format") + + # FIKS: 401 kastes nå UTENFOR try-blokken, slik at vi unngår 500-krasj + if not is_valid: + print(" - ❌ Passordet samsvarer ikke med hashen") + raise HTTPException(status_code=401, detail="Ugyldig brukernavn eller passord") temp_token = jwt.encode( {"sub": admin['username'], "partial": True, "exp": datetime.utcnow() + timedelta(minutes=5)}, diff --git a/kode_eksport_1/backend_scrape_status_py.txt b/kode_eksport_1/backend_scrape_status_py.txt index b3f2397..0a18990 100644 --- a/kode_eksport_1/backend_scrape_status_py.txt +++ b/kode_eksport_1/backend_scrape_status_py.txt @@ -2,7 +2,7 @@ import asyncio import os import asyncpg import smtplib -import re # Ny import for tekst-vasking +import re from datetime import datetime from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart @@ -12,7 +12,6 @@ try: except ImportError: from playwright_stealth import stealth as apply_stealth -from apscheduler.schedulers.asyncio import AsyncIOScheduler from dotenv import load_dotenv load_dotenv() @@ -20,28 +19,21 @@ load_dotenv() DB_URL = "postgresql://teeoff_admin:teeoff_secret_password@db:5432/teeoff" def clean_text(text): - """Fjerner spesialtegn og normaliserer tekst for sammenligning""" return re.sub(r'[^a-zA-Z0-9æøåÆØÅ]', '', text).lower() def interpret_status(text, keyword=None): t_raw = text.lower() if keyword: - # Fuzzy match: Vi renser både tekst og søkeord for bindestreker/mellomrom k_clean = clean_text(keyword) - - # Hvis vi ikke finner søkeordet engang i renset form, gi opp if k_clean not in clean_text(t_raw): return "NOT_FOUND" - # Hvis vi finner det, prøv å isolere teksten rundt det originale ordet - # Vi leter etter det originale keywordet først parts = re.split(re.escape(keyword), t_raw, flags=re.IGNORECASE) if len(parts) > 1: t_raw = parts[1][:150] else: - # Fallback hvis keywordet er delt av HTML-tagger (f.eks 18 hull) - t_raw = t_raw[-200:] # Bruk slutten av teksten hvis ordet er vanskelig å isolere + t_raw = t_raw[-200:] if any(word in t_raw for word in ["stengt", "lukket", "frost", "snø", "is", "closed", "stenger"]): return "stengt" @@ -53,28 +45,34 @@ def interpret_status(text, keyword=None): return "aapen" return "ukjent" -def send_report(changes, warnings): - if not changes and not warnings: return +def send_report(changes, warnings, successes): + if not changes and not warnings and not successes: return subject = f"TeeOff Banestatus Rapport - {datetime.now().strftime('%d.%m.%Y')}" body = "BANESTATUS RAPPORT\n" + "="*30 + "\n\n" - if changes: body += "✅ OPPDATERINGER:\n" + "\n".join(changes) + "\n\n" - if warnings: body += "⚠️ MERKNADER / ADVARSLER:\n" + "\n".join(warnings) + "\n" - msg = MIMEMultipart(); msg['From'] = os.getenv("SMTP_USER"); msg['To'] = os.getenv("EMAIL_TO"); msg['Subject'] = subject + if changes: body += "✅ OPPDATERINGER:\n" + "\n".join(changes) + "\n\n" + if warnings: body += "⚠️ MERKNADER / ADVARSLER:\n" + "\n".join(warnings) + "\n\n" + if successes: body += "🆗 VELLYKKEDE SJEKKER (INGEN ENDRING):\n" + "\n".join(successes) + "\n" + + msg = MIMEMultipart() + msg['From'] = os.getenv("SMTP_USER") + msg['To'] = os.getenv("EMAIL_TO") + msg['Subject'] = subject msg.attach(MIMEText(body, 'plain')) try: with smtplib.SMTP_SSL(os.getenv("SMTP_SERVER"), int(os.getenv("SMTP_PORT"))) as server: server.login(os.getenv("SMTP_USER"), os.getenv("SMTP_PASS")) server.send_message(msg) print("✅ Rapport sendt på e-post.") - except Exception as e: print(f"❌ E-post feil: {e}") + except Exception as e: + print(f"❌ E-post feil: {e}") async def run_daily_scraping(): print(f"🚀 Starter sjekk {datetime.now().strftime('%H:%M:%S')}...") conn = await asyncpg.connect(DB_URL) - facilities = await conn.fetch("SELECT id, name, scrape_status_url, scrape_status_selector FROM facilities WHERE scrape_status_url IS NOT NULL") + facilities = await conn.fetch("SELECT id, name, scrape_status_url, scrape_status_selector, scrape_method FROM facilities WHERE scrape_status_url IS NOT NULL") - changes, warnings = [], [] + changes, warnings, successes = [], [], [] async with async_playwright() as p: browser = await p.chromium.launch(headless=True) @@ -87,17 +85,60 @@ async def run_daily_scraping(): try: print(f"🔍 Besøker {f['name']}...") - await page.goto(f['scrape_status_url'], timeout=60000, wait_until="networkidle") - - # Vent på at innholdet skal lande - await asyncio.sleep(3) + # Endret fra networkidle til domcontentloaded for å unngå Arendal-timeout + await page.goto(f['scrape_status_url'], timeout=60000, wait_until="domcontentloaded") + await asyncio.sleep(3) # Gir Javascript 3 sekunder på å bygge siden - element = await page.query_selector(f['scrape_status_selector']) - if not element: - warnings.append(f"❌ {f['name']}: Fant ikke elementet '{f['scrape_status_selector']}'") - continue + full_text = "" + method = f.get('scrape_method') or 'css_selector' + + if method == 'css_selector': + element = page.locator(f['scrape_status_selector']).first + if await element.count() == 0: + warnings.append(f"❌ {f['name']}: Fant ikke CSS-elementet '{f['scrape_status_selector']}'") + continue + full_text = await element.inner_text() + + elif method == 'iframe_golfbox': + frame = page.frame_locator('iframe[src*="golfbox"]') + element = frame.locator(f['scrape_status_selector']).first + if await element.count() == 0: + warnings.append(f"❌ {f['name']}: Fant ikke elementet '{f['scrape_status_selector']}' i iframen") + continue + full_text = await element.inner_text() + + elif method == 'click_then_css': + # Vi forventer formatet: "knappe_selector||tekst_selector" + parts = f['scrape_status_selector'].split('||') + if len(parts) != 2: + warnings.append(f"❌ {f['name']}: Ugyldig selector for click_then_css (mangler ||)") + continue + + btn_selector, text_selector = parts + + # 1. Finn og klikk på knappen + btn = page.locator(btn_selector).first + if await btn.count() == 0: + warnings.append(f"❌ {f['name']}: Fant ikke knappen å klikke på: '{btn_selector}'") + continue + + await btn.click() + + # 2. Vent 2 sekunder så animasjonen (sidepanelet) rekker å bli ferdig + await asyncio.sleep(2) + + # 3. Les av teksten + element = page.locator(text_selector).first + if await element.count() == 0: + warnings.append(f"❌ {f['name']}: Fant ikke tekstboksen '{text_selector}' etter klikk") + continue + + full_text = await element.inner_text() - full_text = await element.inner_text() + else: + warnings.append(f"⚠️ {f['name']}: Ukjent skrapemetode i databasen: '{method}'") + continue + await conn.execute("UPDATE facilities SET status_updated_at = CURRENT_DATE WHERE id = $1", f['id']) courses = await conn.fetch("SELECT id, name, status, scrape_keyword FROM courses WHERE facility_id = $1", f['id']) @@ -105,7 +146,7 @@ async def run_daily_scraping(): new_status = interpret_status(full_text, c['scrape_keyword']) if new_status == "NOT_FOUND": - warnings.append(f"❓ {f['name']} ({c['name']}): Fant ikke søkeordet '{c['scrape_keyword']}' på siden.") + warnings.append(f"❓ {f['name']} ({c['name']}): Fant ikke søkeordet '{c['scrape_keyword']}' i teksten på siden.") continue old_status = c['status'] or "ukjent" @@ -114,16 +155,19 @@ async def run_daily_scraping(): changes.append(f"🔹 {f['name']} ({c['name']}): {old_status.upper()} ➔ {new_status.upper()}") print(f"✅ Oppdatert status for {f['name']} - {c['name']}") else: + successes.append(f"✅ {f['name']} ({c['name']}): {new_status.upper()}") print(f" - {c['name']}: Ingen endring ({new_status.upper()})") except Exception as e: - warnings.append(f"🔥 {f['name']}: Feil: {str(e)[:100]}") + # Trekker ut kun første linje av feilmeldingen for å unngå massiv og stygg tekst i e-posten + err_msg = str(e).split('\n')[0] + warnings.append(f"🔥 {f['name']}: Feil under skraping: {err_msg}") finally: await page.close() await browser.close() await conn.close() - send_report(changes, warnings) + send_report(changes, warnings, successes) print("🏁 Ferdig.") if __name__ == "__main__": diff --git a/kode_eksport_1/backend_update_admin_py.txt b/kode_eksport_1/backend_update_admin_py.txt new file mode 100644 index 0000000..d896eb0 --- /dev/null +++ b/kode_eksport_1/backend_update_admin_py.txt @@ -0,0 +1,52 @@ +""" +TEE OFF ADMIN PASSWORD UPDATER +--------------------------------------------------------------------------- +FUNKSJON: Genererer SQL-kommando for å KUN oppdatere passord for en eksisterende admin. +STATUS: Påvirker IKKE tofaktor (2FA), e-post eller andre brukere. +--------------------------------------------------------------------------- +""" +from passlib.hash import pbkdf2_sha256 +import getpass +import sys + +def update_admin_password(): + print("\n" + "="*50) + print(" TEE OFF ADMIN PASSORD-OPPDATERER") + print("="*50) + + username = input("Brukernavn på admin som skal oppdateres: ").strip() + safe_username = username.replace("'", "''") + + # Passord-verifisering + while True: + password = getpass.getpass("Skriv inn NYTT passord: ") + password_confirm = getpass.getpass("Gjenta NYTT passord: ") + + if password == password_confirm: + if len(password) < 8: + print("⚠️ Advarsel: Passordet bør være minst 8 tegn.") + print(f"\n[DEBUG] Passord akseptert.") + break + else: + print("❌ Passordene er ikke like. Prøv igjen.\n") + + print("⏳ Genererer PBKDF2-hash...") + password_hash = pbkdf2_sha256.hash(password) + + print("\n✅ GENERERING VELLYKKET!") + print("-" * 50) + print("SLIK OPPDATERER DU PASSORDET TRYGT:") + print("-" * 50) + print("1. Gå inn i databasen:") + print(" docker exec -it teeoff_db psql -U teeoff_admin -d teeoff") + print("\n2. Lim inn denne linjen nøyaktig slik den står:") + print(f"UPDATE admins SET password_hash = '{password_hash}' WHERE username = '{safe_username}';") + print("\n3. Skriv 'exit' for å gå ut.") + print("-" * 50 + "\n") + +if __name__ == "__main__": + try: + update_admin_password() + except KeyboardInterrupt: + print("\nAvbrutt.") + sys.exit(0) \ No newline at end of file diff --git a/kode_eksport_1/frontend_src_app_admin_login_page_tsx.txt b/kode_eksport_1/frontend_src_app_admin_login_page_tsx.txt index a3587fb..9f19401 100644 --- a/kode_eksport_1/frontend_src_app_admin_login_page_tsx.txt +++ b/kode_eksport_1/frontend_src_app_admin_login_page_tsx.txt @@ -39,6 +39,7 @@ export default function AdminLogin() { setError(data.detail || 'Ugyldig pålogging'); } } catch (err) { + console.error("🔥 DEN EKTE FEILEN ER:", err); setError('Systemfeil: Kunne ikke koble til API-et'); } finally { setIsLoading(false); diff --git a/kode_eksport_1/test_tjome_py.txt b/kode_eksport_1/test_tjome_py.txt new file mode 100644 index 0000000..17dcef1 --- /dev/null +++ b/kode_eksport_1/test_tjome_py.txt @@ -0,0 +1,44 @@ +import asyncio +from playwright.async_api import async_playwright + +async def main(): + async with async_playwright() as p: + browser = await p.chromium.launch(headless=True) + page = await browser.new_page() + print("🔍 Går til Tjøme Golfklubb...") + await page.goto('https://tjomegolfklubb.no/', wait_until="domcontentloaded") + await asyncio.sleep(3) + + btn_count = await page.locator("a:has-text('Banestatus')").count() + print(f"🤖 Fant {btn_count} lenker med teksten 'Banestatus'.") + + try: + # Tvinger roboten til å velge den knappen som faktisk er SYNLIG på skjermen + btn = page.locator("a:has-text('Banestatus'):visible").first + await btn.click(timeout=5000) + print("🖱️ Klikket på den synlige Banestatus-knappen!") + await asyncio.sleep(2) + except Exception as e: + print(f"⚠️ Klarte ikke klikke: {str(e).splitlines()[0]}") + + # Henter ut både synlig tekst og "skjult" tekst i koden + synlig_tekst = await page.locator("body").inner_text() + all_tekst = await page.locator("body").text_content() + + print("\n--- RESULTAT ---") + if "stengt" in synlig_tekst.lower(): + print("✅ Suksess! Fant ordet 'stengt' i den SYNLIGE teksten.") + elif "stengt" in all_tekst.lower(): + print("🫣 Fant ordet 'stengt' gjemt i HTML-koden (Panelet åpnet seg ikke skikkelig for roboten).") + idx = all_tekst.lower().find("stengt") + # Fjerner linjeskift for penere utskrift + utdrag = all_tekst[max(0, idx-30):idx+80].replace('\n', ' ') + print(f" Tekstutdrag: '...{utdrag}...'") + else: + print("❌ Fant verken 'stengt' eller 'åpen' på hele siden.") + print(f" (Teksten den leste startet slik: {synlig_tekst[:80].replace(chr(10), ' ')}...)") + print("----------------\n") + + await browser.close() + +asyncio.run(main()) diff --git a/struktur3_dump.txt b/struktur3_dump.txt new file mode 100644 index 0000000..6f8ba73 --- /dev/null +++ b/struktur3_dump.txt @@ -0,0 +1,622 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 15.8 (Debian 15.8-1.pgdg110+1) +-- Dumped by pg_dump version 15.8 (Debian 15.8-1.pgdg110+1) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: tiger; Type: SCHEMA; Schema: -; Owner: teeoff_admin +-- + +CREATE SCHEMA tiger; + + +ALTER SCHEMA tiger OWNER TO teeoff_admin; + +-- +-- Name: tiger_data; Type: SCHEMA; Schema: -; Owner: teeoff_admin +-- + +CREATE SCHEMA tiger_data; + + +ALTER SCHEMA tiger_data OWNER TO teeoff_admin; + +-- +-- Name: topology; Type: SCHEMA; Schema: -; Owner: teeoff_admin +-- + +CREATE SCHEMA topology; + + +ALTER SCHEMA topology OWNER TO teeoff_admin; + +-- +-- Name: SCHEMA topology; Type: COMMENT; Schema: -; Owner: teeoff_admin +-- + +COMMENT ON SCHEMA topology IS 'PostGIS Topology schema'; + + +-- +-- Name: fuzzystrmatch; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS fuzzystrmatch WITH SCHEMA public; + + +-- +-- Name: EXTENSION fuzzystrmatch; Type: COMMENT; Schema: -; Owner: +-- + +COMMENT ON EXTENSION fuzzystrmatch IS 'determine similarities and distance between strings'; + + +-- +-- Name: postgis; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA public; + + +-- +-- Name: EXTENSION postgis; Type: COMMENT; Schema: -; Owner: +-- + +COMMENT ON EXTENSION postgis IS 'PostGIS geometry and geography spatial types and functions'; + + +-- +-- Name: postgis_tiger_geocoder; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS postgis_tiger_geocoder WITH SCHEMA tiger; + + +-- +-- Name: EXTENSION postgis_tiger_geocoder; Type: COMMENT; Schema: -; Owner: +-- + +COMMENT ON EXTENSION postgis_tiger_geocoder IS 'PostGIS tiger geocoder and reverse geocoder'; + + +-- +-- Name: postgis_topology; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS postgis_topology WITH SCHEMA topology; + + +-- +-- Name: EXTENSION postgis_topology; Type: COMMENT; Schema: -; Owner: +-- + +COMMENT ON EXTENSION postgis_topology IS 'PostGIS topology spatial types and functions'; + + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: admins; Type: TABLE; Schema: public; Owner: teeoff_admin +-- + +CREATE TABLE public.admins ( + id integer NOT NULL, + username character varying(50) NOT NULL, + password_hash text NOT NULL, + otp_secret character varying(32), + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + email character varying(255) +); + + +ALTER TABLE public.admins OWNER TO teeoff_admin; + +-- +-- Name: admins_id_seq; Type: SEQUENCE; Schema: public; Owner: teeoff_admin +-- + +CREATE SEQUENCE public.admins_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.admins_id_seq OWNER TO teeoff_admin; + +-- +-- Name: admins_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: teeoff_admin +-- + +ALTER SEQUENCE public.admins_id_seq OWNED BY public.admins.id; + + +-- +-- Name: courses; Type: TABLE; Schema: public; Owner: teeoff_admin +-- + +CREATE TABLE public.courses ( + id integer NOT NULL, + facility_id integer, + name character varying(255) NOT NULL, + holes integer, + par integer, + length_meters integer, + course_type character varying(255), + architect character varying(255), + status character varying(255), + is_main_course boolean DEFAULT true, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + tee_boxes jsonb, + scrape_keyword text +); + + +ALTER TABLE public.courses OWNER TO teeoff_admin; + +-- +-- Name: courses_id_seq; Type: SEQUENCE; Schema: public; Owner: teeoff_admin +-- + +CREATE SEQUENCE public.courses_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.courses_id_seq OWNER TO teeoff_admin; + +-- +-- Name: courses_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: teeoff_admin +-- + +ALTER SEQUENCE public.courses_id_seq OWNED BY public.courses.id; + + +-- +-- Name: facilities; Type: TABLE; Schema: public; Owner: teeoff_admin +-- + +CREATE TABLE public.facilities ( + id integer NOT NULL, + name character varying(255) NOT NULL, + slug character varying(255) NOT NULL, + description text, + established_year integer, + season character varying(255), + address character varying(255), + zipcode character varying(50), + city character varying(255), + county character varying(255), + lat double precision, + lng double precision, + email character varying(255), + phone character varying(255), + website_url character varying(255), + golfbox_booking_url character varying(255), + golfbox_tournament_url character varying(255), + facebook_url character varying(255), + instagram_url character varying(255), + weather_url character varying(255), + webcam_url character varying(255), + golfamore boolean DEFAULT false, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + image_url character varying(500), + amenities jsonb, + greenfee jsonb, + architect text, + membership jsonb, + vtg jsonb, + video_url text, + baneguide_url text, + logo_url text, + flyfoto_url text, + guest_requirements text, + status_updated_at date, + gallery jsonb, + faqs jsonb DEFAULT '[]'::jsonb, + shotzoom jsonb DEFAULT '[]'::jsonb, + front_image_url text, + nsg_url text, + nsg_description text, + nsg_data jsonb DEFAULT '{}'::jsonb, + golfamore_data jsonb DEFAULT '{}'::jsonb, + ngf_number integer, + golfbox_club_id integer, + golfbox_booking_id text, + facebook_id text, + instagram_place_id text, + tournament_url text, + footnote text, + social_links jsonb DEFAULT '[]'::jsonb, + webcam_html text, + length_meters integer, + navn_standard_medlemskap text, + standard_medlemskap integer, + standard_medlemskap_kommentarer text, + navn_rimeligste_alternativ text, + rimeligste_alternativ integer, + rimeligste_alternativ_kommentarer text, + medlemskap_url text, + banetype text, + scrape_status_url text, + scrape_status_selector text, + scrape_method character varying(50) DEFAULT 'css_selector'::character varying +); + + +ALTER TABLE public.facilities OWNER TO teeoff_admin; + +-- +-- Name: facilities_id_seq; Type: SEQUENCE; Schema: public; Owner: teeoff_admin +-- + +CREATE SEQUENCE public.facilities_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.facilities_id_seq OWNER TO teeoff_admin; + +-- +-- Name: facilities_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: teeoff_admin +-- + +ALTER SEQUENCE public.facilities_id_seq OWNED BY public.facilities.id; + + +-- +-- Name: facility_images; Type: TABLE; Schema: public; Owner: teeoff_admin +-- + +CREATE TABLE public.facility_images ( + id integer NOT NULL, + facility_id integer, + image_url character varying(255) NOT NULL, + display_order integer DEFAULT 0, + sort_order integer DEFAULT 0 +); + + +ALTER TABLE public.facility_images OWNER TO teeoff_admin; + +-- +-- Name: facility_images_id_seq; Type: SEQUENCE; Schema: public; Owner: teeoff_admin +-- + +CREATE SEQUENCE public.facility_images_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.facility_images_id_seq OWNER TO teeoff_admin; + +-- +-- Name: facility_images_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: teeoff_admin +-- + +ALTER SEQUENCE public.facility_images_id_seq OWNED BY public.facility_images.id; + + +-- +-- Name: hole_lengths; Type: TABLE; Schema: public; Owner: teeoff_admin +-- + +CREATE TABLE public.hole_lengths ( + id integer NOT NULL, + hole_id integer, + tee_id integer, + length_meters integer +); + + +ALTER TABLE public.hole_lengths OWNER TO teeoff_admin; + +-- +-- Name: hole_lengths_id_seq; Type: SEQUENCE; Schema: public; Owner: teeoff_admin +-- + +CREATE SEQUENCE public.hole_lengths_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.hole_lengths_id_seq OWNER TO teeoff_admin; + +-- +-- Name: hole_lengths_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: teeoff_admin +-- + +ALTER SEQUENCE public.hole_lengths_id_seq OWNED BY public.hole_lengths.id; + + +-- +-- Name: holes; Type: TABLE; Schema: public; Owner: teeoff_admin +-- + +CREATE TABLE public.holes ( + id integer NOT NULL, + course_id integer, + hole_number integer NOT NULL, + par integer, + hcp_index integer, + lengths jsonb +); + + +ALTER TABLE public.holes OWNER TO teeoff_admin; + +-- +-- Name: holes_id_seq; Type: SEQUENCE; Schema: public; Owner: teeoff_admin +-- + +CREATE SEQUENCE public.holes_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.holes_id_seq OWNER TO teeoff_admin; + +-- +-- Name: holes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: teeoff_admin +-- + +ALTER SEQUENCE public.holes_id_seq OWNED BY public.holes.id; + + +-- +-- Name: tees; Type: TABLE; Schema: public; Owner: teeoff_admin +-- + +CREATE TABLE public.tees ( + id integer NOT NULL, + course_id integer, + name character varying(50) NOT NULL, + cr_men numeric(4,1), + slope_men integer, + cr_women numeric(4,1), + slope_women integer +); + + +ALTER TABLE public.tees OWNER TO teeoff_admin; + +-- +-- Name: tees_id_seq; Type: SEQUENCE; Schema: public; Owner: teeoff_admin +-- + +CREATE SEQUENCE public.tees_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.tees_id_seq OWNER TO teeoff_admin; + +-- +-- Name: tees_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: teeoff_admin +-- + +ALTER SEQUENCE public.tees_id_seq OWNED BY public.tees.id; + + +-- +-- Name: admins id; Type: DEFAULT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.admins ALTER COLUMN id SET DEFAULT nextval('public.admins_id_seq'::regclass); + + +-- +-- Name: courses id; Type: DEFAULT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.courses ALTER COLUMN id SET DEFAULT nextval('public.courses_id_seq'::regclass); + + +-- +-- Name: facilities id; Type: DEFAULT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.facilities ALTER COLUMN id SET DEFAULT nextval('public.facilities_id_seq'::regclass); + + +-- +-- Name: facility_images id; Type: DEFAULT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.facility_images ALTER COLUMN id SET DEFAULT nextval('public.facility_images_id_seq'::regclass); + + +-- +-- Name: hole_lengths id; Type: DEFAULT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.hole_lengths ALTER COLUMN id SET DEFAULT nextval('public.hole_lengths_id_seq'::regclass); + + +-- +-- Name: holes id; Type: DEFAULT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.holes ALTER COLUMN id SET DEFAULT nextval('public.holes_id_seq'::regclass); + + +-- +-- Name: tees id; Type: DEFAULT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.tees ALTER COLUMN id SET DEFAULT nextval('public.tees_id_seq'::regclass); + + +-- +-- Name: admins admins_email_key; Type: CONSTRAINT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.admins + ADD CONSTRAINT admins_email_key UNIQUE (email); + + +-- +-- Name: admins admins_pkey; Type: CONSTRAINT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.admins + ADD CONSTRAINT admins_pkey PRIMARY KEY (id); + + +-- +-- Name: admins admins_username_key; Type: CONSTRAINT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.admins + ADD CONSTRAINT admins_username_key UNIQUE (username); + + +-- +-- Name: courses courses_pkey; Type: CONSTRAINT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.courses + ADD CONSTRAINT courses_pkey PRIMARY KEY (id); + + +-- +-- Name: facilities facilities_pkey; Type: CONSTRAINT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.facilities + ADD CONSTRAINT facilities_pkey PRIMARY KEY (id); + + +-- +-- Name: facilities facilities_slug_key; Type: CONSTRAINT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.facilities + ADD CONSTRAINT facilities_slug_key UNIQUE (slug); + + +-- +-- Name: facility_images facility_images_pkey; Type: CONSTRAINT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.facility_images + ADD CONSTRAINT facility_images_pkey PRIMARY KEY (id); + + +-- +-- Name: hole_lengths hole_lengths_pkey; Type: CONSTRAINT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.hole_lengths + ADD CONSTRAINT hole_lengths_pkey PRIMARY KEY (id); + + +-- +-- Name: holes holes_pkey; Type: CONSTRAINT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.holes + ADD CONSTRAINT holes_pkey PRIMARY KEY (id); + + +-- +-- Name: tees tees_pkey; Type: CONSTRAINT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.tees + ADD CONSTRAINT tees_pkey PRIMARY KEY (id); + + +-- +-- Name: facilities unique_slug; Type: CONSTRAINT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.facilities + ADD CONSTRAINT unique_slug UNIQUE (slug); + + +-- +-- Name: courses courses_facility_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.courses + ADD CONSTRAINT courses_facility_id_fkey FOREIGN KEY (facility_id) REFERENCES public.facilities(id) ON DELETE CASCADE; + + +-- +-- Name: hole_lengths hole_lengths_hole_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.hole_lengths + ADD CONSTRAINT hole_lengths_hole_id_fkey FOREIGN KEY (hole_id) REFERENCES public.holes(id) ON DELETE CASCADE; + + +-- +-- Name: hole_lengths hole_lengths_tee_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.hole_lengths + ADD CONSTRAINT hole_lengths_tee_id_fkey FOREIGN KEY (tee_id) REFERENCES public.tees(id) ON DELETE CASCADE; + + +-- +-- Name: holes holes_course_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.holes + ADD CONSTRAINT holes_course_id_fkey FOREIGN KEY (course_id) REFERENCES public.courses(id) ON DELETE CASCADE; + + +-- +-- Name: tees tees_course_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: teeoff_admin +-- + +ALTER TABLE ONLY public.tees + ADD CONSTRAINT tees_course_id_fkey FOREIGN KEY (course_id) REFERENCES public.courses(id) ON DELETE CASCADE; + + +-- +-- PostgreSQL database dump complete +-- + diff --git a/test_tjome.py b/test_tjome.py new file mode 100644 index 0000000..17dcef1 --- /dev/null +++ b/test_tjome.py @@ -0,0 +1,44 @@ +import asyncio +from playwright.async_api import async_playwright + +async def main(): + async with async_playwright() as p: + browser = await p.chromium.launch(headless=True) + page = await browser.new_page() + print("🔍 Går til Tjøme Golfklubb...") + await page.goto('https://tjomegolfklubb.no/', wait_until="domcontentloaded") + await asyncio.sleep(3) + + btn_count = await page.locator("a:has-text('Banestatus')").count() + print(f"🤖 Fant {btn_count} lenker med teksten 'Banestatus'.") + + try: + # Tvinger roboten til å velge den knappen som faktisk er SYNLIG på skjermen + btn = page.locator("a:has-text('Banestatus'):visible").first + await btn.click(timeout=5000) + print("🖱️ Klikket på den synlige Banestatus-knappen!") + await asyncio.sleep(2) + except Exception as e: + print(f"⚠️ Klarte ikke klikke: {str(e).splitlines()[0]}") + + # Henter ut både synlig tekst og "skjult" tekst i koden + synlig_tekst = await page.locator("body").inner_text() + all_tekst = await page.locator("body").text_content() + + print("\n--- RESULTAT ---") + if "stengt" in synlig_tekst.lower(): + print("✅ Suksess! Fant ordet 'stengt' i den SYNLIGE teksten.") + elif "stengt" in all_tekst.lower(): + print("🫣 Fant ordet 'stengt' gjemt i HTML-koden (Panelet åpnet seg ikke skikkelig for roboten).") + idx = all_tekst.lower().find("stengt") + # Fjerner linjeskift for penere utskrift + utdrag = all_tekst[max(0, idx-30):idx+80].replace('\n', ' ') + print(f" Tekstutdrag: '...{utdrag}...'") + else: + print("❌ Fant verken 'stengt' eller 'åpen' på hele siden.") + print(f" (Teksten den leste startet slik: {synlig_tekst[:80].replace(chr(10), ' ')}...)") + print("----------------\n") + + await browser.close() + +asyncio.run(main())