Før LLM og AII skal synkronisere
This commit is contained in:
parent
bacfb51e50
commit
1ca1868e11
16 changed files with 1062 additions and 58 deletions
|
|
@ -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
|
||||
EMAIL_TO=erol.haagenrud@teeoff.no
|
||||
GEMINI_API_KEY=AIzaSyDX_WCvZcH3Z8xRpH-XWaoeVYWuE0Wrlog
|
||||
Binary file not shown.
|
|
@ -10,4 +10,5 @@ apscheduler
|
|||
python-dotenv
|
||||
python-jose[cryptography]
|
||||
passlib[bcrypt]
|
||||
pyotp
|
||||
pyotp
|
||||
google-generativeai
|
||||
|
|
@ -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}'")
|
||||
|
|
|
|||
47
backend/test_login.py
Normal file
47
backend/test_login.py
Normal file
|
|
@ -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())
|
||||
85
backend/update_admin.py
Normal file
85
backend/update_admin.py
Normal file
|
|
@ -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)
|
||||
12
fil-tre.txt
12
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
|
||||
|
|
|
|||
|
|
@ -83,8 +83,8 @@ export default function AdminLogin() {
|
|||
<form onSubmit={step === 1 ? handleLogin : handleVerify2FA} className="space-y-4">
|
||||
{step === 1 ? (
|
||||
<>
|
||||
<input type="text" placeholder="Brukernavn eller E-post" className="w-full p-5 bg-gray-50 rounded-2xl border-none ring-1 ring-gray-100 outline-none focus:ring-2 focus:ring-[#8bc34a] transition-all text-sm font-bold text-[#11280f]" onChange={e => setFormData({...formData, username: e.target.value})} required />
|
||||
<input type="password" placeholder="Passord" className="w-full p-5 bg-gray-50 rounded-2xl border-none ring-1 ring-gray-100 outline-none focus:ring-2 focus:ring-[#8bc34a] transition-all text-sm font-bold text-[#11280f]" onChange={e => setFormData({...formData, password: e.target.value})} required />
|
||||
<input type="text" placeholder="Brukernavn eller E-post" className="w-full p-5 bg-gray-50 rounded-2xl border-none ring-1 ring-gray-100 outline-none focus:ring-2 focus:ring-[#8bc34a] transition-all text-sm font-bold text-[#11280f]" onChange={e => setFormData(prevState => ({...prevState, username: e.target.value}))} required />
|
||||
<input type="password" placeholder="Passord" className="w-full p-5 bg-gray-50 rounded-2xl border-none ring-1 ring-gray-100 outline-none focus:ring-2 focus:ring-[#8bc34a] transition-all text-sm font-bold text-[#11280f]" onChange={e => setFormData(prevState => ({...prevState, password: e.target.value}))} required />
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
try:
|
||||
generate_admin()
|
||||
except KeyboardInterrupt:
|
||||
print("\nAvbrutt.")
|
||||
sys.exit(0)
|
||||
|
|
@ -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)},
|
||||
|
|
|
|||
|
|
@ -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 <strong>hull</strong>)
|
||||
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__":
|
||||
|
|
|
|||
52
kode_eksport_1/backend_update_admin_py.txt
Normal file
52
kode_eksport_1/backend_update_admin_py.txt
Normal file
|
|
@ -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)
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
44
kode_eksport_1/test_tjome_py.txt
Normal file
44
kode_eksport_1/test_tjome_py.txt
Normal file
|
|
@ -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())
|
||||
622
struktur3_dump.txt
Normal file
622
struktur3_dump.txt
Normal file
|
|
@ -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
|
||||
--
|
||||
|
||||
44
test_tjome.py
Normal file
44
test_tjome.py
Normal file
|
|
@ -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())
|
||||
Loading…
Reference in a new issue